Home Handling Shared Resources using Dispatch Barriers
Post
Cancel

Handling Shared Resources using Dispatch Barriers

Handling Thread-Unsafe Shared Resource

In the last post, we have discussed about creating thread-safe singleton objects. But creating a thread-safe singleton does not solve all the issues. If your singleton object uses any mutable object like NSMutableArray, then you need to consider whether that object is itself thread-safe or not. There is a misconception that the Foundation framework is thread-safe and the Application / UI Kit framework is not. Unfortunately, this is somewhat misleading. Each framework has areas that are thread-safe and areas that are not thread-safe. Apple maintains a helpful list of the numerous Foundation framework classes which are not thread-safe.

The Problem

Lets take an example, we have a class DocumentManager, which manages all our document read, write handling. And the DocumentManager class has been implemented as a singleton. This singleton object uses a NSMutableArray property for keeping all the document names, which is a Thread-Unsafe class. In DocumentManager, we have two functions addDocumentName and allDocs. Although many threads can read an instance of NSMutableArray simultaneously without issue, It’s not safe to let one thread modify the array while another is reading it. Our singleton doesn’t prevent this condition from happening in its current state. To see the problem, have a look at code, which has been reproduced below:

- (void)addDocumentName:(NSString*)docName { if (docName) [_arrDocs addObject:docName]; }

This is a write method, which modifies the mutable array object by adding a document name into it. Now take a look at getter method:

- (NSArray*)allDocs { return [NSArray arrayWithArray:_arrDocs]; }

This is a read method as it’s reading the mutable array property. It makes an immutable copy for the caller in order to defend against the caller mutating the array inappropriately, but none of this provides any protection against one thread calling the the write method addDocumentName: while simultaneously another thread calls the read method allDocs. This is the classic software development Readers-Writers Problem. GCD provides an elegant solution of creating a Readers-writer lock using dispatch barriers.

Dispatch Barriers

It allows you to make thread-unsafe object to thread-safe. It creates a synchronisation point for a code block executing in a concurrent dispatch queue. Dispatch barriers are a group of functions acting as a serial queue style objects 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. The diagram below illustrates the effect of barrier functions on various asynchronous blocks:

[caption id=”attachment_178” align=”aligncenter” width=”660”]Dispatch Barrier Execution of Dispatch Barrier[/caption]

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 monopolise 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.

Since the only decent choice above is the custom concurrent queue, you’ll need to create one of your own to handle your barrier function and separate the read and write functions. The concurrent queue will allow multiple read operations simultaneously. Creating a custom concurrent queue is easy: just pass DISPATCH_QUEUE_CONCURRENT to the dispatch_queue_create function. Open your singleton class, and add the following private property to the class extension category:

@property (nonatomic, strong) dispatch_queue_t concurrentDocumentQueue;

Now replace addDocumentName function with below implementation:

- (void)addDocumentName:(NSString*)docName { if (docName) { // 1 dispatch_barrier_async(self. concurrentDocumentQueue, ^{ // 2 [_arrDocs addObject:docName]; // 3 }); } }

Here’s how your new write function works:

  1. Check that there’s a valid document name before performing all the following work.
  2. Add the write operation using your custom queue. When the critical section executes at a later time this will be the only item in your queue to execute.
  3. This is the actual code which adds the object to the array. Since it’s a barrier block, this block will never run simultaneously with any other block in concurrentDocumentQueue.

This takes care of the write method, but we also need to implement the allDocs read method and instantiate concurrentDocumentQueue. To ensure thread safety with the writer side of matters, you need to perform the read on the concurrentDocumentQueue queue. You need to return from the function though, so you can’t dispatch asynchronously to the queue because that wouldn’t necessarily run before the reader function returns. In this case, dispatch_sync would be an excellent candidate. 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.

Now replace allDocs method with the following implementation:

- (NSArray*)allDocs { __block NSArray *array; // 1 dispatch_sync(self.concurrentDocumentQueue, ^{ // 2 array = [NSArray arrayWithArray:_arrDocs]; // 3 }); return array; }

Here’s your read function. Taking each numbered comment in turn, you’ll find the following:

  1. The __block keyword allows objects to be mutable inside a block. Without this, array would be read-only inside the block and your code wouldn’t even compile.
  2. Dispatch synchronously onto the concurrentDocumentQueue to perform the read.
  3. Store the document array in array and return it.

Finally, you need to instantiate your concurrentDocumentQueue property. Since we are working on a singleton class, so we will make the changes in the sharedManager function so that it instantiate only once. Change sharedManager method to instantiate the custom concurrent queue like so:

+ (instancetype)sharedManager { static DocumentManager *sharedDocumentManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedDocumentManager = [[DocumentManager alloc] init]; sharedDocumentManager->_arrDocs = [NSMutableArray array]; // ADD THIS: sharedDocumentManager->_concurrentDocumentQueue = dispatch_queue_create(“com.mycompany.ThreadySafety.documentQueue”,DISPATCH_QUEUE_CONCURRENT);  }); return sharedDocumentManager; }

This initialises concurrentDocumentQueue as a concurrent queue using dispatch_queue_create. The first parameter is a reversed DNS style naming convention; make sure it’s descriptive since this can be helpful when debugging. The second parameter specifies whether you want your queue to be serial or concurrent. Note: When searching for examples on the web, you’ll often see people pass 0 or NULL as the second parameter of dispatch_queue_create. This is a dated way of creating a serial dispatch queue; it’s always better to be specific with your parameters. Your mutable property now thread safe. No matter where or how you read or write this property, you can be confident that it will be done in a safe manner with no amusing surprises.

You can download the source code which contains all the code snippet used in this post. Some parts of this post is derived from Ray Wenderlich Tutorials. Thanks for reading. :)
This post is licensed under CC BY 4.0 by the author.