Skip to content

Latest commit

 

History

History
479 lines (294 loc) · 22.1 KB

File metadata and controls

479 lines (294 loc) · 22.1 KB

Grand Central Dispatch (Part 2)

Minute-by-Minute

Elapsed Time Activity
0:00 0:05 Objectives
0:05 0:20 Initial Exercise
0:25 0:25 Overview I
0:50 0:10 BREAK
1:00 0:25 Overview II
1:25 0:25 In Class Activity I
TOTAL 1:50

Learning Objectives (5 min)

By the end of this lesson, you should be able to...

  1. Identify and describe:
  • when to use Synchronous and Asynchronous
  • Sync and Deadlocks
  • Critical Section
  • Thread Safety
  • GCD's provided serial and concurrent DispatchQueues
  • QoS Priority
  • how the Main Queue fits in GCD

Initial Exercise (20 min)

Part 1 — Diagramming Concurrent Task Execution

Individually - Diagram

Scenario:

  • Your app fetches images from the Internet, then processes them for display through a sepia tone filter.
  • Fetching and processing images is negatively impacting performance, especially scrolling in the table view.

TODO: Diagram how you would use what you know so far about currency in iOS to improve your app's performance. In your diagram, be sure to point out:

  1. the point in time on the relevant queue(s) where an image is downloaded
  2. the point where the image is filtered
  3. the point at which the image is presented to the user, and the queue on which this would take place

        activity_diag_template

Part 2 — Calling main.sync from Background Thread

Let's examine the code in the snippet below:

  func someFunction() {
    DispatchQueue.global().async {  // A) Run the following code block asynchronously on a non-UI thread
          let url = URL(string: imageUrl)
          do {
              let data = try Data(contentsOf: url!)
                  DispatchQueue.main.sync { // B) Run the code below synchronously on the main queue
                      self.imageIcon.image = UIImage(data: data)
                  }
              }
    }

Q: What exactly is happening in this code?

Q: When might this be useful?

Source: https://www.reddit.com/r/iOSProgramming/comments/7n9e9f/what_is_the_difference_between/

Overview/TT I (25 min)

When to use Async?

In asynchronous execution by the .async method, the method call returns immediately, ordering the task to be done but not waiting for it to finish.

Asynchronous tasks are started by one thread but actually run on a different thread, taking advantage of additional processor resources to finish their work more quickly.

from: Apple docs

Use .async when your app does not need to wait until the operation inside the block is finished.

As mentioned, long-running tasks such as image/data processing, local or remote data fetching, and other network calls are typically good candidates for running tasks .async.

If the viewDidLoad() method of your View Controller has too much work to do on the main queue, this can often result in long wait times before the view appears. If possible, it’s best to offload some work to be done in the background using .async, if it’s not absolutely essential at load time for the view.

When to use Sync?

In synchronous execution by .sync method, the current thread waits until the task is finished before the method call returns.

Use .sync when your app needs to wait until a task is finished.

Example scenarios include:

  • to ensure a particular function is not called a second time before its first invocation is finished.
  • to wait for an operation inside a block to finish before you can use the data processed by the block.

Sync and Deadlocks

Never Call .sync on Current Queue

Do not call the dispatch_sync (aka, .sync) function from a task that is executing on the same queue that you pass to your function call. Doing so will deadlock the queue.

If you need to dispatch to the current queue, do so asynchronously using the dispatch_async (.async) function.

Source: Apple docs

Deadlock

A deadlock occurs when two or more tasks are waiting on each other to finish and get stuck in a never-ending cycle. Neither can proceed until the other completes; but, since neither can proceed, neither will finish.

A deadlock can occur even when the perpetually-waiting tasks are on the same thread.

We'll see an example of a deadlock a bit later in this lesson....

The Main queue

When your app starts, a main dispatch queue is automatically created for you.

The main queue:

  • is a serial queue that's responsible for your UI. (We'll cover serial and concurrent queues in the next lesson.)
  • is closely associated with the main thread and its call stack. The main queue only executes code on the main thread.

REMEMBER: You should never perform UI updates on any queue other than the main queue. And you must always access UI Kit classes on the main thread.

How the Main Queue Fits

Our updated diagram of the structure inside the runtime process of an iOS app simply illustrates that the main queue is associated directly with the main thread and its call stack, and that the system also creates queues for non-UI tasks.

(1) When an iOS app starts, the system automatically creates the app's:

  • main queue
  • main thread
  • and the corresponding call stack that the main thread manages.

(2) The main thread, again, allocates your app's Application object in its stack frame, which in turn executes its delegate methods on its AppDelegate object in their respective stack frames, and so on...

(3) Notice that, though the system also creates a pool of additional threads for potential non-UI tasks and their corresponding call stacks, what actually happens is here is that additional dispatch queues are created (to which the system will assign a thread from the pool, as needed):

        iOS_runtime_process_with-queues.png

DispatchQueue.main (class variable)

Because it's used so often, Apple has made it available as a class variable, which you access via DispatchQueue.main.

Example showing .async called on the built-in DispatchQueue.main property:

  import Foundation

  var value: Int = 2

  DispatchQueue.main.async {
      for i in 0...3 {
          value = i
          print("\(value) ✴️")
      }
  }

  for i in 4...6 {
      value = i
      print("\(value) ✡️")
  }

  DispatchQueue.main.async {
      value = 9
      print(value)
  }

Concurrency and the Main Queue

In general, look for opportunities to take long-running, non-UI task and run them asynchronously on a queue other than the main queue.

The pseudocode example below illustrates the typical steps to running a non-UI tasks asynchronously on a background queue/thread:

  1. create a queue
  2. submit a task to it to run asynchronously
  3. when it is complete, you redirect control flow back to the main thread to update the UI.
  // Somewhere inside a class...
  let queue = DispatchQueue(label: "com.makeschool.queue")

  // Somewhere in your function
  queue.async {
    // Call slow non-UI methods here
    DispatchQueue.main.async {
      // Update the UI here
    }
  }

Never Call .sync on the Main Queue

Just as with the current queue, you never want to execute a code block synchronously against the main queue either. Doing so could cause your app to crash or it might simply degrade your app's performance by locking your UI.

For example, this statement:

  DispatchQueue.main.sync {
    // Some synchronous code block
  }

...will cause the following events:

  1. sync queues the block in the main queue.
  2. sync blocks the thread of the main queue until the block finishes executing.
  3. but sync will wait forever because the thread where the block is intended to run is blocked. The submitted code block will not even start until the thread is unblocked, and the thread can't be unblocked until that same code block completes.

In the diagram below:

  • A code block identified as Code Block A was submitted synchronously to the Main Queue.
  • At the point in which DispatchQueue.main.sync is called, the Main Queue is blocked awaiting the completion of the code block (Code Block A) which called .sync.
  • But Code Block A cannot even start executing until the Main Queue is unblocked.

sync_deadlock_base

The key to understanding this is that .sync does not execute tasks/blocks, it only queues them. Execution will happen on a future iteration of the run loop. If the thread is blocked until some condition occurs (i.e., some block of code completes), no future iteration of the run loop can occur that condition is met and the queue and its thread are unblocked.

Source: https://medium.com/swift-india/parallel-programming-with-swift-part-1-4-df7caac564ae

Overview/TT II (25 min)

Key Concepts

Before we delve deeper into GCD's DispatchQueues, let's explore a couple of related concepts...

Critical Section

Multiple, concurrent accesses to shared resources can lead to unexpected or erroneous behavior. So, parts of the program where the shared resource is accessed are protected from concurrent access.

This protected section is called the critical section (or critical region).

The code in the critical section:

  • accesses shared variables/resources and has to be executed as an atomic action.
  • should not be executed by more than one process at a time.

Typically, the critical section accesses a shared resource — such as a data structure, a peripheral device, or a network connection — that would not operate correctly in the context of multiple concurrent accesses.

For example, a critical section might manipulate a particular variable that can become corrupt if it is accessed by concurrent processes.

In the diagram below, if Process 1 executes the code in the Critical Section to read a shared variable — while Process 2 needs to write to the same variable — Process 1 might get either the old or new value of the variable:

       Critical_section_fg

Examples:

  1. Classic Example — A bank account shared by two people.

If Person 1 checks the account balance at the same time that Person 2 executes some transaction (eg., withdraws money), that transaction may not be reflected in the balance that Person 1 sees.

To ensure the balance reported is always accurate, any code accessing the variable holding the balance can be protected from concurrent access.

  1. A shared document.

When two or more people are updating a shared document, access to the document can be temporarily limited to each active contributor — even if only for an instance — to prevent additional contributors from making changes before the doc is autosaved.

     Sources:
       - Apple
       - wikipedia

Thread Safety

Thread safe code:

  • can be safely called from multiple threads or concurrent tasks without causing any problems (data corruption, crashing, etc).
  • is guaranteed to be free of race conditions when accessed by multiple threads simultaneously.

An example of thread safe code: A Dictionary or an Array that is declared as a constant (with let) — because it is read-only,, you can access it from multiple threads at the same time without issue.

Code that is not thread safe must only be run in one context at a time — it must not be accessed from more than one thread at a time.

Example: A Dictionary or an Array declared as a var is not thread safe and should only be accessed from one thread at a time.

DispatchQueues (cont'd)

GCDs DispatchQueues possess several defining attributes:

  • Queues can be either serial or concurrent
  • FIFO — This guarantees that the first task added to the queue is the first task started in the queue, the second task added will be the second to start, and so on...
  • Thread Safe — All dispatch queues are themselves thread-safe: you can access them from multiple threads simultaneously. On of the key benefits of GCD is that DispatchQueues can provide thread-safety to parts of your own code.

Remember: The decision of when to start a task is entirely up to GCD.

  • If the execution time of one task overlaps with another, it’s up to GCD to determine if it should run on a different core, if one is available, or instead to perform a context switch to run a different task.

Serial Queues

Serial queues guarantee that only one task runs at any given time.

Serial Queues:

  • only have a single thread associated with them and thus only allow a single task to be executed at any given time.
  • execute tasks in the order they are submitted, one at a time. 1

Since no two tasks in a serial queue can ever run concurrently, there is no risk they might access the same critical section concurrently; that protects the critical section from race conditions with respect to those tasks only. So if the only way to access that critical section is via a task submitted to that dispatch queue, then you can be sure that the critical section is safe.

serial_queue

     Source:
        https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2

The Main Queue is a Serial Queue
As mentioned, when an iOS app launches, the system automatically creates a serial queue called the main queue and binds it to the application’s main thread.

Any tasks assigned in the main queue and are executed serially, one at a time, on the main thread in their turn.

Any time you write code, such as the following snippet, to access the main queue you are accessing the same, singular system-created queue, and any code you submit to that queue is executed serially on the main thread:

  let mainQueue   = DispatchQueue.main

This main queue is the queue to use for sending messages to UIViews and and all other UI-related tasks.

Concurrent Queues

On the other hand, a concurrent queue is able to utilize as many threads as the system has resources for.

For Concurrent Queues:

  • threads will be created and released as needed for a concurrent queue.
  • multiple tasks can run at the same time.
  • tasks are guaranteed to start in the order they were added.

GCD and Concurrent Queues 1
But tasks can finish in any order and you have no knowledge of the time it will take for the next block to start, nor the number of blocks that are running at any given time. (This is entirely up to GCD.)

The decision of when and where to start a block is entirely up to GCD, too. If the execution time of one block overlaps with another, it’s up to GCD to determine if it should run on a different core, if one is available, or instead to perform a context switch to a different block of code.

If your iOS device is completely bogged down and your app is competing for resources, it may only be capable of running a single task.

concurrent_queue

     Source:
        https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1


1 GCD controls the execution timing. You won’t know the amount of time between one task ending and the next one beginning.

Types of Queues

The GCD library (libdispatch) automatically creates several queues with different priority levels that execute several tasks concurrently, selecting the optimal number of tasks to run based on the operating environment.

GCD provides three main types of queues:

  1. The Main Queue — a serial queue that runs on the main thread.

  2. Global Dispatch Queues — These a concurrent queues provided by the system which the system shares with any processes requesting them. There are four Global Dispatch Queues with different priorities:

  • high
  • default
  • low
  • background
  1. Custom Queues — Queues you create. You can create them as either serial or concurrent queues.
  • In actuality, the system will assign one of the Global Queues to handle your custom queue's activities.
QoS Priority

When setting up Global Dispatch Queues, you do not specify the priority directly; instead, you are required to specify a Quality of Service (Qos) level (as a class property) which guides GCD into determining the priority level to give the task.

GCD offers you four Quality of Service (Qos) classes:

  • .userInteractive

  • .userInitiated

  • .utility

  • .background

In Class Activity I (25 min)

Part 1 — In Groups of 3 — A Jigsaw Puzzle Exercise

For the QoS class assigned to your team, fill in the following categories (columns) in the slide provided:

  • Type of work and focus of QoS
  • Duration of work to be performed
  • Example Use Case

Team 1: .userInteractive

Team 2: .userInitiated

Team 3: .utility

Team 4: .background

HINT: See Resources below for details

Resources:
https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html https://developer.apple.com/documentation/dispatch/dispatchqos

Part 1 — Class Discussion

Q: Does Asynchronous mean Concurrent?

After Class

  1. Research:
  • The Dining Philosophers Problem
  • The Critical Section Problem
  • Race Condition(s)
  • The two secondary GCD QoS priority classes:     - .default
        - .unspecified
  1. Assignment:
  • Execute the following tutorial (part 1 only):

https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2

Wrap Up (5 min)

  • Start working on assigned current tutorial
  • Complete reading

Additional Resources

  1. Slides
  2. Modernizing Grand Central Dispatch Usage - a vide from Apple
  3. Critical_section - wikipedia
  4. lock() - from Apple docs
  5. Peterson Algorithm - wikipedia
  6. Thread_safety - wikipedia