iOS Concurrent Programming

Bored 🥑
13 min readAug 2, 2017

--

This account is no longer in use. Please checkout my latest articles in iOS Interview Preparation Complete Guide.

Basic Concepts

Thread: A context of execution of a program, scheduled for running by the Operating System. Any number of these can exist at once.

Concurrency: A state of execution in which multiple threads are performing tasks on the same shared resource.

Reentrancy: A state of execution in which a function re-enters itself, either via explicit recursion, software/hardware interrupt, or other methods.

Atomicity: A property of an operation such that it is guaranteed to finish or fail, but can never end up or produce an intermediate state, or an invalid state.

Thread Safe: A function is thread safe if, and only if, invalid state cannot be caused, or observed, by entering a state of concurrency.

Re-entrant: A function is re-entrant if, and only if, invalid state cannot be caused, or observed, by entering a state of reentrancy.

Race Conditions

Most race conditions are caused by shared mutable state. Mutable state does not necessarily mean variables, either. State can be modified outside the realm of your application, including the file system, network, syscalls, and more.

States and Copying

One of the best ways to avoid mutable state is to have strict guidelines on how you manage state as a whole. In general, we adhere to three rules:

  1. Separate your state from the code that modifies it. This allows you to have a clear separation of concerns between your reading and mutation, and allows you to better reason about threading in your code.
  2. Pass anything mutable by copy. Passing by reference creates the possibility of concurrent resource modification, and you will need to do some form of synchronization to ensure this doesn’t happen.
  3. When in doubt, just lock. It may be slower, but it’s better than having a 1-in-1000 race condition that breaks your app.
  4. Remember that global state is bad, and you should avoid it at all costs (yes, this includes singletons).

Apple’s Frameworks

One of the most common mistakes even experienced iOS/Mac developers make is accessing parts of UIKit/AppKit on background threads. It’s very easy to make the mistake of setting properties like image from a background thread, because their content is being requested from the network in the background anyway. Apple’s code is performance-optimized and will not warn you if you change properties from different threads.

In the case of an image, a common symptom is that your change is picked up with a delay. But if two threads set the image at the same time, it’s likely that your app will simply crash, because the currently set image could be released twice. Since this is timing dependent, it usually will crash when used by your customers and not during development. Xcode 9 has embedded a main thread tracker

The Deallocation Problem

When you start a secondary thread, it’s common for that thread to retain the target object. This happens in numerous circumstances, including:

  • when you start a secondary thread with any of the following methods:
  • -performSelectorInBackground:withObject:
  • -performSelector:onThread:withObject:waitUntilDone:
  • -performSelector:onThread:withObject:waitUntilDone:modes:
  • when you start a secondary thread with NSThread
  • when you run a block asynchronously and the block references self or an instance variable

When a secondary thread retains the target object, you have to ensure that the thread releases that reference before the main thread releases its last reference to the object. If you don’t do this, the last reference to the object is released by the secondary thread, which means that the object’s -dealloc method runs on that secondary thread. This is problematic if the object's -dealloc method does things that are not safe to do on a secondary thread, something that's common for UIKit objects like a view controller.

An example of the deallocation problem

The problem is that UI objects should be deallocated on the main thread, because some of them might perform changes to the view hierarchy in dealloc. . Consistent use of __weak and not accessing ivars in async blocks/operations helps.

Collection Classes

Immutable objects are generally thread-safe. Once you create them, you can safely pass these objects to and from threads. On the other hand, mutable objects are generally not thread-safe.

In fact, it’s fine to use them from different threads, as long as access is serialized within a queue. Remember that methods might return a mutable variant of a collection object even if they declare their return type as immutable. It’s good practice to write something like return [array copy] to ensure the returned object is in fact immutable.

Atomicity

A nonatomic property setter might look like this:

- (void)setUserName:(NSString *)userName {
if (userName != _userName) {
[userName retain];
[_userName release];
_userName = userName;
}
}

This is the variant with manual retain/release; however, the ARC-generated code looks similar. When we look at this code it’s obvious why this means trouble when setUserName: is called concurrently. We could end up releasing _userNametwice, which can corrupt memory and lead to hard-to-find bugs.

As a general rule though, if every operation you perform on a specified address is atomic, no reads to that address can put your application in an invalid state. These primitives, then, when combined with atomic properties, can ensure than any single field cannot be in an invalid state. Note that the object as a whole may still be able to be in an invalid state, as every atomic operation you perform is entirely independent of all other atomic operations that are being executed on other addresses.

Your Own Classes

Using atomic properties alone won’t make your classes thread-safe. It will only protect you against race conditions in the setter, but won’t protect your application logic. Consider the following snippet:

if (self.contents) {
CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,
(__bridge CFStringRef)self.contents, NULL);
// draw string
}

From time to time, the application crashed with a EXCBADACCESS, when the contents property was set to nil after the check. A simple fix for this issue would be to capture the variable:

NSString *contents = self.contents;
if (contents) {
CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,
(__bridge CFStringRef)contents, NULL);
// draw string
}

Practical Thread-Safe Design

Before trying to make something thread-safe, think hard if it’s necessary. Make sure it’s not premature optimization. If it’s anything like a configuration class, there’s no point in thinking about thread safety.

Now there’s code that definitely should be thread-safe; a good example is a caching class. A good approach is to use a concurrent dispatch_queue as read/write lock to maximize performance and try to only lock the areas that are really necessary. Once you start using multiple queues for locking different parts, things get tricky really fast.

// header
@property (nonatomic, strong) NSMutableSet *delegates;

// in init
_delegateQueue = dispatch_queue_create("com.PSPDFKit.cacheDelegateQueue",
DISPATCH_QUEUE_CONCURRENT);

- (void)addDelegate:(id<PSPDFCacheDelegate>)delegate {
dispatch_barrier_async(_delegateQueue, ^{
[self.delegates addObject:delegate];
});
}

- (void)removeAllDelegates {
dispatch_barrier_async(_delegateQueue, ^{
self.delegates removeAllObjects];
});
}

- (void)callDelegateForX {
dispatch_sync(_delegateQueue, ^{
[self.delegates enumerateObjectsUsingBlock:^(id<PSPDFCacheDelegate> delegate, NSUInteger idx, BOOL *stop) {
// Call delegate
}];
});
}

Unless addDelegate: or removeDelegate: is called thousand times per second, a simpler and cleaner approach is the following:

// header
@property (atomic, copy) NSSet *delegates;

- (void)addDelegate:(id<PSPDFCacheDelegate>)delegate {
@synchronized(self) {
self.delegates = [self.delegates setByAddingObject:delegate];
}
}

- (void)removeAllDelegates {
@synchronized(self) {
self.delegates = nil;
}
}

- (void)callDelegateForX {
[self.delegates enumerateObjectsUsingBlock:^(id<PSPDFCacheDelegate> delegate, NSUInteger idx, BOOL *stop) {
// Call delegate
}];
}

Synchronization

When it comes to thread safety, a good design is the best protection you have. Avoiding shared resources and minimizing the interactions between your threads makes it less likely for those threads to interfere with each other. A completely interference-free design is not always possible, however. In cases where your threads must interact, you need to use synchronization tools to ensure that when they interact, they do so safely.

Atomic Operations

Atomic operations are a simple form of synchronization that work on simple data types. The advantage of atomic operations is that they do not block competing threads. For simple operations, such as incrementing a counter variable, this can lead to much better performance than taking a lock.

Memory Barriers and Volatile Variables

A memory barrier is a type of nonblocking synchronization tool used to ensure that memory operations occur in the correct order. A memory barrier acts like a fence, forcing the processor to complete any load and store operations positioned in front of the barrier before it is allowed to perform load and store operations positioned after the barrier. Memory barriers are typically used to ensure that memory operations by one thread (but visible to another) always occur in an expected order. The lack of a memory barrier in such a situation might allow other threads to see seemingly impossible results.

Volatile variables apply another type of memory constraint to individual variables. The compiler often optimizes code by loading the values for variables into registers. For local variables, this is usually not a problem. If the variable is visible from another thread however, such an optimization might prevent the other thread from noticing any changes to it. Applying the volatile keyword to a variable forces the compiler to load that variable from memory each time it is used. You might declare a variable as volatile if its value could be changed at any time by an external source that the compiler may not be able to detect.

Challenges of Concurrent Programming

Readers and writers problem

Dispatch barriers are a group of functions acting as a serial-style bottleneck when working with concurrent queues. Using GCD’s barrier API ensures that the submitted block is the only item executed on the specified queue for that particular time. This means that all items submitted to the queue prior to the dispatch barrier must complete before the block will execute.

When the block’s turn arrives, the barrier executes the block and ensures that the queue does not execute any other blocks during that time. Once finished, the queue returns to its default implementation. GCD provides both synchronous and asynchronous barrier functions.

Notice how in normal operation the queue acts just like a normal concurrent queue. But when the barrier is executing, it essentially acts like a serial queue. That is, the barrier is the only thing executing. After the barrier finishes, the queue goes back to being a normal concurrent queue.

Here’s when you would — and wouldn’t — use barrier functions:

  • Custom Serial Queue: A bad choice here; barriers won’t do anything helpful since a serial queue executes one operation at a time anyway.
  • Global Concurrent Queue: Use caution here; this probably isn’t the best idea since other systems might be using the queues and you don’t want to monopolize them for your own purposes.
  • Custom Concurrent Queue: This is a great choice for atomic or critical areas of code. Anything you’re setting or instantiating that needs to be thread safe is a great candidate for a barrier.

dispatch_sync() synchronously submits work and waits for it to be completed before returning. Use dispatch_sync to track of your work with dispatch barriers, or when you need to wait for the operation to finish before you can use the data processed by the block. If you're working with the second case, you'll sometimes see a __block variable written outside of the dispatch_sync scope in order to use the processed object returned outside the dispatch_sync function.

You need to be careful though. Imagine if you call dispatch_sync and target the current queue you're already running on. This will result in a deadlock because the call will wait to until the block finishes, but the block can't finish (it can't even start!) until the currently executing task is finished, which can't! This should force you to be conscious of which queue you're calling from — as well as which queue you're passing in.

Here’s a quick overview of when and where to use dispatch_sync:

  • Custom Serial Queue: Be VERY careful in this situation; if you’re running in a queue and call dispatch_sync targeting the same queue, you will definitely create a deadlock.
  • Main Queue (Serial): Be VERY careful for the same reasons as above; this situation also has potential for a deadlock condition.
  • Concurrent Queue: This is a good candidate to sync work through dispatch barriers or when waiting for a task to complete so you can perform further processing.

Priority Inversion

Priority inversion describes a condition where a lower priority task blocks a higher priority task from executing, effectively inverting task priorities. Since GCD exposes background queues with different priorities, including one which even is I/O throttled, it’s good to know about this possibility.

The problem can occur when you have a high-priority and a low-priority task share a common resource. When the low-priority task takes a lock to the common resource, it is supposed to finish off quickly in order to release its lock and to let the high-priority task execute without significant delays. Since the high-priority task is blocked from running as long as the low-priority task has the lock, there is a window of opportunity for medium-priority tasks to run and to preempt the low-priority task, because the medium-priority tasks have now the highest priority of all currently runnable tasks. At this moment, the medium-priority tasks hinder the low-priority task from releasing its lock, therefore effectively gaining priority over the still waiting, high-priority tasks.

In general, don’t use different priorities. Often you will end up with high-priority code waiting on low-priority code to finish. When you’re using GCD, always use the default priority queue (directly, or as a target queue). If you’re using different priorities, more likely than not, it’s actually going to make things worse.

GCD Queues

GCD provides dispatch queues to handle blocks of code; these queues manage the tasks you provide to GCD and execute those tasks in FIFO order. This guarantees that 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 down the line.

All dispatch queues are themselves thread-safe in that you can access them from multiple threads simultaneously. The benefits of GCD are apparent when you understand how dispatch queues provide thread-safety to parts of your own code. The key to this is to choose the right kind of dispatch queue and the right dispatching function to submit your work to the queue.

Serial Queues

Tasks in serial queues execute one at a time, each task starting only after the preceding task has finished. As well, you won’t know the amount of time between one block ending and the next one beginning

The execution timing of these tasks is under the control of GCD; the only thing you’re guaranteed to know is that GCD executes only one task at a time and that it executes the tasks in the order they were added to the queue

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.

Concurrent Queues

Tasks in concurrent queues are guaranteed to start in the order they were added…and that’s about all you’re guaranteed! Items 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. Again, this is entirely up to GCD.

Queue Types

First, the system provides you with a special serial queue known as the main queue. Like any serial queue, tasks in this queue execute one at a time. However, it’s guaranteed that all tasks will execute on the main thread, which is the only thread allowed to update your UI. This queue is the one to use for sending messages to UIViews or posting notifications.

The system also provides you with several concurrent queues. These are known as the Global Dispatch Queues. There are currently four global queues of different priority: background, low, default, and high. Be aware that Apple’s APIs also uses these queues, so any tasks you add won’t be the only ones on these queues.

Thread Safe Singleton

dispatch_once() executes a block once and only once in a thread safe manner. Different threads that try to access the critical section — the code passed to dispatch_once — while a thread is already in this section are blocked until the critical section completes.

Quality of Service

When setting up the global concurrent queues, you don’t specify the priority directly. Instead you specify a Quality of Service (QoS) class property. This will indicate the task’s importance and guide GCD into determining the priority to give to the task.

The QoS classes are:

  • User-interactive: This represents tasks that need to be done immediately in order to provide a nice user experience. Use it for UI updates, event handling and small workloads that require low latency. The total amount of work done in this class during the execution of your app should be small. This should run on the main thread.
  • User-initiated: The represents tasks that are initiated from the UI and can be performed asynchronously. It should be used when the user is waiting for immediate results, and for tasks required to continue user interaction. This will get mapped into the high priority global queue.
  • Utility: This represents long-running tasks, typically with a user-visible progress indicator. Use it for computations, I/O, networking, continous data feeds and similar tasks. This class is designed to be energy efficient. This will get mapped into the low priority global queue.
  • Background: This represents tasks that the user is not directly aware of. Use it for prefetching, maintenance, and other tasks that don’t require user interaction and aren’t time-sensitive. This will get mapped into the background priority global queue.

--

--