Synchronizing Read/Write Access to an Instance Variable for High Performance in iOS

I have a singleton that can be accessed on multiple threads, should each of its array have a separate dispatch queue?

In answer to the one reader-writer vs one for each collection, it probably doesn’t matter. But there are a few considerations:

  1. The only potential problem with only one reader-writer is that a block on one collection will block them all. So, if you’re doing anything computationally expensive in the synchronization of one or more of the collections, you might want a separate reader-writer for each so it doesn’t adversely affect the performance of the others.

    Then again, you never want to do anything computationally expensive with reader-writer pattern, anyway. The “writer” is an asynchronous barrier, so that means that it will block any subsequent “readers” until the write is done. And if accessing the readers from the main queue, that will adversely affect the performance on the main thread (or from wherever you’re reading).

    So, if it’s computationally expensive, you wouldn’t use reader-writer, anyway. (You’d probably want an asynchronous fetch method, breaking you out of the reader-writer pattern.) And if you’re not doing anything computationally expensive, then there’s little benefit to using separate reader-writer for each collection.

  2. Setting up multiple reader-writers is fine if the three collections are entirely independent (as your simple example might suggest). But if there are any dependencies between the three collections (e.g. you’re reading from one to update the other), the code as potential to quickly become a mess.

So, bottom line, I’d lean towards a single reader-writer for the whole class for the sake of simplicity. But if you already have a thread-safe collection class that is using reader-writer (or any synchronization mechanism, for that matter) internally, then that it’s probably fine to use that within this class that has these three collections (as long as you don’t have any hairy dependencies between your three collections).


Needless to say, the above applies to any object shared amongst multiple threads, not just to singletons. The fact that you happen to be using using a singleton is irrelevant to the question at hand.

The reader writer lock I wrote backed by GCD code causes a deadlock in parallel test

The problem is not the reader/writer pattern, per se, but rather because the general thread explosion in this code. See “Thread Explosion Causing Deadlock” discussion in WWDC 2015 video Building Responsive and Efficient Apps with GCD. The WWDC 2016 Concurrent Programming With GCD in Swift 3 is also a good video. In both of those links, I’m dropping you off in the relevant portion of the video, but both are worth watching in their entirety.

Bottom line, you are exhausting the very limited number of worker threads in GCD’s thread pool. There are only 64. But you’ve got 100 writes with barriers, which means that the dispatched block can’t run until everything else on that queue is done. These are interspersed with 100 reads, which because they are synchronous, will block the worker thread from which you dispatched it until it returns.

Let’s reduce this down to something simpler that manifests the problem:

dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 100; i++) {
dispatch_async(queue2, ^{
dispatch_barrier_async(queue1, ^{
NSLog(@"barrier async %d", i);
});
dispatch_sync(queue1, ^{
NSLog(@"sync %d", i);
});
});
}
NSLog(@"done dispatching all blocks to queue1");

That produces something like:

starting

done dispatching all blocks to queue1

barrier async 0

sync 0

And it deadlocks.

But if we constrain it so that no more than, say 30 items can run concurrently on queue2, then the problem goes away:

dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(30);

for (int i = 0; i < 100; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue2, ^{
dispatch_barrier_async(queue1, ^{
NSLog(@"barrier async %d", i);
});
dispatch_sync(queue1, ^{
NSLog(@"sync %d", i);
});
dispatch_semaphore_signal(semaphore);
});
}
NSLog(@"done dispatching all blocks to queue1");

Or, another approach is to use dispatch_apply, which effectively a parallelized for loop, but limits the number of concurrent tasks at any given moment to the number of cores on your machine (keeping us well below the threshold to exhaust worker threads):

dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);

dispatch_apply(100, queue2, ^(size_t i) {
dispatch_barrier_async(queue1, ^{
NSLog(@"barrier async %ld", i);
});
dispatch_sync(queue1, ^{
NSLog(@"sync %ld", i);
});
});
NSLog(@"done dispatching all blocks to queue1");

Do we need to declare a property atomic if we use GCD?

Using atomic is one way to synchronize a property being used from multiple threads. But there are many mechanisms for synchronizing access from multiple threads, and atomic is one with fairly limited utility. I'd suggest you refer to the Synchronization chapter of the Threading Programming Guide for a fuller discussion of alternatives (and even that fails to discuss other contemporary patterns such as GCD serial queues and reader-writer pattern with a custom, concurrent queue).

Bottom line, atomic is, by itself, neither necessary nor sufficient to ensure thread safety. In general, it has some limited utility when dealing with some simple, fundamental data type (Booleans, NSInteger) but is inadequate when dealing with more complicated logic or when dealing with mutable objects.

In short, do not assume that you should use atomic whenever you use GCD. In fact, if you use GCD, that generally obviates the need for atomic, which, in fact, will unnecessary and adversely impact performance in conjunction with GCD. So, if you have some property being accessed from multiple threads, you should synchronize it, but the choice of which synchronization technique to employ is a function of the the specific details of the particular situation, and GCD often is a more performant and more complete solution.



Related Topics



Leave a reply



Submit