Parallel Programming with Swift — Part 2/4

Target Queue, Dispatch Group, Barrier, Work Item, Semaphore & Dispatch Source

Aaina jain
Swift India
13 min readSep 25, 2018

--

Credits: freepik.com

This article is part 2 of Parallel Programming with Swift. As in Part 1, we looked into Dispatch Queue and system provided queues. In this post, I am going to focus on another way of defining tasks and powerful APIs provided by GCD.

If you want to have a look on all parts of series:

Concurrency & GCD — Parallel Programming with Swift — Part 1/4

GCD — Parallel Programming with Swift — Part 2/4

Agenda:

  1. Target Queue
  2. DispatchGroup
  3. DispatchWorkItem
  4. DispatchBarrier
  5. DispatchSemaphore
  6. DispatchSources

1. Target Queue

A custom dispatch queue doesn’t execute any work, it just passes work to the target queue. By default, target queue of custom dispatch queue is a default-priority global queue. Since Swift 3, once a dispatch queue is activated, it cannot be mutated anymore. The target queue of a custom queue can be set by the setTarget(queue:) function. Setting a target on an activated queue will compile but then throw an error in runtime. Fortunately, DispatchQueue initializer accepts other arguments. If for whatever reason, you still need to set the target on an already created queue, you can do that by using the initiallyInactive attribute available since iOS 10.

DispatchQueue(label: "queue", attributes: .initiallyInactive)

That will allow you to modify it until you activate it.

activate() method of the DispatchQueue class will make the task to execute. As the as the queue hasn’t been marked as a concurrent one, they’ll run in a serial order. To make queue concurrent we need to specify:

DispatchQueue(label: "queue", attributes: [.initiallyInactive, .concurrent])

You can pass to target queue any other dispatch queue, even another custom queue, so long as you never create a cycle. This function can be used to set the priority of a custom queue by simply setting its target queue to a different global queue. Only the global concurrent queues and the main queue get to execute blocks. All other queues must (eventually) target one of these special queues.

You might have noticed target queue in Dispatch API or frameworks you use. I found the use of target queue in RxSwift library.

Target queue uses:

  • If you set your custom queue's target to be the low priority global queue, all work on your custom queue will execute with low priority and the same with the high priority global queue.
  • To target a custom queue to the main queue. This will cause all blocks submitted to that custom queue to run on the main thread. The advantage of doing this instead of simply using the main queue directly is that your custom queue can be independently suspended and resumed, and could potentially be retargeted onto a global queue afterward.
  • To target custom queues for other custom queues. This will force multiple queues to be serialized with respect to each other, and essentially creates a group of queues which can all be suspended and resumed together by suspending/resuming the queue that they target.

**Get Current Queue Name**

API provides us a function to check whether a call is in the main thread or not. But there is no way to get current queue name. By using below snippet you can print current queue name anywhere.

2. DispatchGroup

With dispatch groups we can group together multiple tasks and either wait for them to be completed or be notified once they are complete. Tasks can be asynchronous or synchronous and can even run on different queues. Dispatch groups are managed by DispatchGroup object.

Let’s consider a scenario: I have to download couple of images and I want to notify user when all images are downloaded.

With this implementation it’s difficult to get notify on completion as tasks are executing asynchronously. Here DispatchGroup comes into picture:

DispatchGroup allows for aggregate synchronization of work. It can be used to submit multiple different work items or blocks and track when they all complete, even though they might run on different queues. Only necessary thing is to make balanced calls to enter() and leave() on a dispatch group to have it synchronize our tasks. Call enter() to manually notify the group that a task has started and leave to notify that work has been done. You can call group.wait() too which blocks the current thread until the group’s tasks have completed.. There are two ways to call completion block:

  1. Using wait() and then execute completion block on main queue Or
  2. Call group notify()
  3. wait(timeout:). This blocks the current thread, but after the timeout specified, continues anyway. To create a timeout object of type DispatchTime, the syntax .now() + 1 will create a timeout one second from now.
  4. wait(timeout:) returns an enum that can be used to determine whether the group completed, or timed out.

Some other use cases:

  • You need to run two distinct network calls. Only after they both have returned you have the necessary data to parse the responses.
  • An animation is running, parallel to a long database call. Once both of those have finished, you’d like to hide a loading spinner.
  • The network API you’re using is too quick. Now your pull to the refresh gesture doesn’t seem to be working, even though it is. The API call returns so quickly that the refresh control dismisses itself as soon as it has finished the appearance animation — which makes it like it’s not refreshing. To solve this, we can add a faux delay. i.e. we can wait for both some minimum time threshold, and the network call, before hiding the refresh control.

2. DispatchWorkItem:

One common misconception about GCD is that “once you schedule a task it can’t be cancelled, you need to use the Operation API for that”. With iOS 8 & macOS 10.10 DispatchWorkItem was introduced, which provides this exact functionality in a easy to use API.

DispatchWorkItem encapsulates work that can be performed. A work item can be dispatched onto a DispatchQueue and within a DispatchGroup. A DispatchWorkItem can also be set as a DispatchSource event, registration, or cancel handler.

In other words, A DispatchWorkItem encapsulates block of code that can be dispatched to any queue.

A dispatch work item has a cancel flag. If it is cancelled before running, the dispatch queue won’t execute it and will skip it. If it is cancelled during its execution, the cancel property return true. In that case, we can abort the execution. Also work items can notify a queue when their task is completed.

Note: GCD doesn’t perform preemptive cancelations. To stop a work item that has already started, you have to test for cancelations yourself.

If you don’t want instant execution of Work Item, not an issue, wait function is there to help you out. We can pass some time interval for the delay in execution.

OOPS, there are two types of wait function in DispatchWorkItem. Which one to use? Confused?

func wait(timeout: DispatchTime) -> DispatchTimeoutResult

func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult

DispatchTime is basically the time according to device clock and if the device goes to sleep, the clock sleeps too. A perfect couple.

But DispatchWallTime is the time according to wall clock, who doesn’t sleep at all, A Night Watch.

After the execution, we can notify the same queue or other queue by calling func notify(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default, queue: DispatchQueue, execute: @escaping () -> Void)

DispatchQoS class works with DispatchQueue, It helps in categorization of the tasks according to their importance. The task with highest priority will execute first as the system assigns more resources for faster execution. But the task with lower priority will execute later as it requires less number of resources and energy. It helps the application to become more responsive and energy efficient.

DispatchWorkItemFlags is basically a set of unique options that can customize the behaviour of DispatchWorkItem. Depending on the flag value you provided, it decides whether to create a new thread or need to create a barrier.

One queue is cancelling task after 2 seconds which interrupts image download task as we perform check for isCancelled in for loop.

3. DispatchBarrier

One frequent concern with singletons is that often they’re not thread safe as singletons are often used from multiple controllers accessing the singleton instance at the same time. Thread safe code can be safely called from concurrent tasks without causing any problems such as data corruption. Code that is not thread safe can only run in one context at a time.

There are two thread safety cases to consider: during initialization of the singleton instance and during reads and writes to the instance.

Initialization turns out to be the easy case because of how Swift initializes static variables. It initializes static variables when they are first accessed, and it guarantees initialization is atomic.

A critical section is a piece of code that must not execute concurrently, that is, from two threads at once. This is usually because the code manipulates a shared resource such as a variable that can become corrupt if it’s accessed by concurrent processes.

When submitted to a a global queue or to a queue not created with the .concurrent attribute, barrier blocks behave identically to *blocks submitted with the async()/sync() API.

Let’s take a problem:

In this example, multiple threads trying to manipulate value variable at the same time and because of that wrong value is getting printed.

objc.io

This is a race condition or classic readers writers problem where blocks on many queues are trying to modify mutable value. As a result of this it’s printing wrong value in block. Swift provides an elegant solution creating a read/write lock using DispatchBarrier.

Credits: Raywenderlich

A dispatch barrier allows us to create a synchronization point within a concurrent dispatch queue. In normal operation, the queue acts just like a normal concurrent queue. But when the barrier is executing, it acts as a serial queue. After the barrier finishes, the queue goes back to being a normal concurrent queue.

GCD takes note of which blocks of code are submitted to the queue before barrier call, and when they all have completed it will call the passed in barrier block. Also, any further blocks that are submitted to the queue will not be executed until after the barrier block has completed. The barrier call, however, returns immediately and execute this block asynchronously.

Technically when we submit a DispatchWorkItem or block to a dispatch queue, we set a flag to indicate that it should be the only item executed on the specified queue for that particular time. All items submitted to the queue prior to the dispatch barrier must complete before this DispatchWorkItem will execute. When the barrier is executed it is the only one task being executed and the queue does not execute any other tasks during that time. Once a barrier is finished, the queue returns to its default behavior.

If the queue is a serial queue or one of the global concurrent queues, the barrier would not work. Using barriers in a custom concurrent queue is a good choice for handling thread safety in critical areas of code.

4. DispatchSemaphore

In multithreaded programming, it’s important to make threads wait. They must wait for exclusive access to a resource. One way to make threads wait and put them to sleep inside the kernel so that they no longer take any CPU time — is with a semaphore. Semaphores were invented by Dijkstra back in the early 1960s.

Semaphores gives us the ability to control access to a shared resource by multiple threads. A shared resource can be a variable, or a task such as downloading an image from url, reading from a database etc.

A semaphore consists of a threads queue and a counter value (of type Int).

Threads queue is used by the semaphore to keep track on waiting threads in FIFO order.

Counter value is used by the semaphore to decide if a thread should get access to a shared resource or not. The counter value changes when we call signal() or wait() functions.

Request the shared resource:

Call wait() each time before using the shared resource. We are basically asking the semaphore if the shared resource is available or not. If not, we will wait.

Release the shared resource

Call signal() each time after using the shared resource. We are basically signaling the semaphore that we are done interacting with the shared resource.

Calling wait() perform the following work:

  • Decrement semaphore counter by 1.
  • If the resulting value is less than zero, thread is blocked and will go into waiting state.
  • If the resulting value is equal or bigger than zero, code will get executed without waiting.

Calling signal() perform the following work:

  • Increment semaphore counter by 1.
  • If the previous value was less than zero, this function unblock the thread currently waiting in the thread queue.
  • If the previous value is equal or bigger than zero, it means thread queue is empty, no one is waiting.

This below image shows perfect example of Semaphore.

Credits: isocpp

Let’s implement Semaphore in a single queue Swift

If you noticed in above code, first wait() and then signal() is called on Semaphore.

Let’s take another example of using Semaphore while downloading image.

Now that we understand how semaphores work, let’s go over a scenario that is more realistic for an app, and that is, downloading 6 images from a url.

First we create a concurrent queue that will be used for executing our image downloading blocks of code.

Second, we create a semaphore and we set it with initial counter value of 2, as we decided to download 2images at a time in order not to take too much CPU time at once.

Third, we iterate 6 times using a for loop. On each iteration we do the following: wait() → download image → signal()

Let’s track the semaphore counter for a better understanding:

  • 2 (Our initial value)
  • 1 (Image 1 wait, since value >= 0, start image download)
  • 0 (image 2 wait, since value >= 0, start image download)
  • -1 (image 3 wait, since value < 0, add to queue)
  • -2 (image 4 wait, since value < 0, add to queue)
  • -3 (image 5 wait, since value < 0, add to queue)
  • -4 (image 6 wait, since value < 0, add to queue)
  • -3 (image 1 signal, last value < 0, wake up image 3 and pop it from queue)
  • - 2 (image 2 signal, last value < 0, wake up image 4 and pop it from queue)
  • -1 (image 3 signal, last value < 0, wake up image 5 and pop it from queue)
  • 0 (image 4 signal, last value < 0, wake up image 6 and pop it from queue)

From this sequence, you can see when one thread starts executing the sequence, the other thread must wait until the first one ends. It doesn’t matter at which point of the sequence the second thread will send the wait() request, it will always have to wait until the other thread is done.

It’s better to use Semaphores only among threads of same priority else it could ended up into Priority Inversion problem.

DispatchSources

Dispatch Sources are a convenient way to handle system level asynchronous events like kernel signals or system, file and socket related events using event handlers.

Dispatch sources can be used to monitor the following types of system events:

  • Timer Dispatch Sources: Used to generate periodic notifications (DispatchSourceTimer).
  • Signal Dispatch Sources: Used to handle UNIX signals (DispatchSourceSignal).
  • Memory Dispatch Sources: Used to register for notifications related to the memory usage status (DispatchSourceMemoryPressure).
  • Descriptor Dispatch Sources: Descriptor sources sends notifications related to a various file- and socket-based operations, such as:
  1. signal when data is available for reading;
  2. signal when it is possible to write data;
  3. files delete, move, or rename;
  4. files meta information change.

(DispatchSourceFileSystemObject, DispatchSourceRead, DispatchSourceWrite).

This enables us to easily build developer tools that have “live editing” features.

  • Process dispatch sources: Used to monitor external process for some events related to their execution state (DispatchSourceProcess). Process-related events, such as
  1. a process exits;
  2. a process issues a fork or exec type of call;
  3. a signal is delivered to the process.
  • Mach related dispatch sources: Used to handle events related to the IPC facilities of the Mach kernel (DispatchSourceMachReceive, DispatchSourceMachSend).
  • Custom events that we trigger: You can define custom dispatch source by conforming to DispatchSourceProtocol

Let’s see some use cases:

  • NSTimer` runs on main thread which needs main run loop to execute. If you want to execute NSTimer on background thread, you can’t. In this situation DispatchSourceTimer could be used. A dispatch timer source, fires an event when the time interval has been completed, which then fires a pre-set callback all on the same queue. Read more….
  • If you have to monitor your files for changes there’s an easy way to do it using DispatchSource Descriptor.

Note:

Once a dispatch task has started running, neither cancelling or suspending the task/queue/work item will stop it. Cancelling and suspending operations only effect tasks that haven’t yet been called. If you must do this, you will need to manually check the cancelled state at appropriate moments during the task’s execution, and exit the task if needed.

Thanks for reading article. If you have any doubt please add in below comment section.

You can catch me at:

Linkedin: Aaina Jain

Twitter: __aainajain

--

--