What Advantage(S) Does Dispatch_Sync Have Over @Synchronized

What advantage(s) does dispatch_sync have over @synchronized?

Wow. OK -- My original performance assessment was flat out wrong. Color me stupid.

Not so stupid. My performance test was wrong. Fixed. Along with a deep dive into the GCD code.

Update: Code for the benchmark can be found here: https://github.com/bbum/StackOverflow Hopefully, it is correct now. :)

Update2: Added a 10 queue version of each kind of test.

OK. Rewriting the answer:

• @synchronized() has been around for a long time. It is implemented as a hash lookup to find a lock that is then locked. It is "pretty fast" -- generally fast enough -- but can be a burden under high contention (as can any synchronization primitive).

dispatch_sync() doesn't necessarily require a lock, nor does it require the block to be copied. Specifically, in the fastpath case, the dispatch_sync() will call the block directly on the calling thread without copying the block. Even in the slowpath case, the block won't be copied as the calling thread has to block until execution anyway (the calling thread is suspended until whatever work is ahead of the dispatch_sync() is finished, then the thread is resumed). The one exception is invocation on the main queue/thread; in that case, the block still isn't copied (because the calling thread is suspended and, therefore, using a block from the stack is OK), but there is a bunch of work done to enqueue on the main queue, execute, and then resume the calling thread.

• dispatch_async() required that the block be copied as it cannot execute on the current thread nor can the current thread be blocked (because the block may immediately lock on some thread local resource that is only made available on the line of code after the dispatch_async(). While expensive, dispatch_async() moves the work off the current thread, allowing it to resume execution immediately.

End result -- dispatch_sync() is faster than @synchronized, but not by a generally meaningful amount (on a '12 iMac, nor '11 mac mini -- #s between the two are very different, btw... joys of concurrency). Using dispatch_async() is slower than both in the uncontended case, but not by much. However, use of 'dispatch_async()' is significantly faster when the resource is under contention.

@synchronized uncontended add: 0.14305 seconds
Dispatch sync uncontended add: 0.09004 seconds
Dispatch async uncontended add: 0.32859 seconds
Dispatch async uncontended add completion: 0.40837 seconds
Synchronized, 2 queue: 2.81083 seconds
Dispatch sync, 2 queue: 2.50734 seconds
Dispatch async, 2 queue: 0.20075 seconds
Dispatch async 2 queue add completion: 0.37383 seconds
Synchronized, 10 queue: 3.67834 seconds
Dispatch sync, 10 queue: 3.66290 seconds
Dispatch async, 2 queue: 0.19761 seconds
Dispatch async 10 queue add completion: 0.42905 seconds

Take the above with a grain of salt; it is a micro-benchmark of the worst kind in that it does not represent any real world common usage pattern. The "unit of work" is as follows and the execution times above represent 1,000,000 executions.

- (void) synchronizedAdd:(NSObject*)anObject
{
@synchronized(self) {
[_a addObject:anObject];
[_a removeLastObject];
_c++;
}
}

- (void) dispatchSyncAdd:(NSObject*)anObject
{
dispatch_sync(_q, ^{
[_a addObject:anObject];
[_a removeLastObject];
_c++;
});
}

- (void) dispatchASyncAdd:(NSObject*)anObject
{
dispatch_async(_q, ^{
[_a addObject:anObject];
[_a removeLastObject];
_c++;
});
}

(_c is reset to 0 at the beginning of each pass and asserted to be == to the # of test cases at the end to ensure that the code is actually executing all the work before spewing the time.)

For the uncontended case:

start = [NSDate timeIntervalSinceReferenceDate];
_c = 0;
for(int i = 0; i < TESTCASES; i++ ) {
[self synchronizedAdd:o];
}
end = [NSDate timeIntervalSinceReferenceDate];
assert(_c == TESTCASES);
NSLog(@"@synchronized uncontended add: %2.5f seconds", end - start);

For the contended, 2 queue, case (q1 and q2 are serial):

    #define TESTCASE_SPLIT_IN_2 (TESTCASES/2)
start = [NSDate timeIntervalSinceReferenceDate];
_c = 0;
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_apply(TESTCASE_SPLIT_IN_2, serial1, ^(size_t i){
[self synchronizedAdd:o];
});
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_apply(TESTCASE_SPLIT_IN_2, serial2, ^(size_t i){
[self synchronizedAdd:o];
});
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
end = [NSDate timeIntervalSinceReferenceDate];
assert(_c == TESTCASES);
NSLog(@"Synchronized, 2 queue: %2.5f seconds", end - start);

The above are simply repeated for each work unit variant (no tricksy runtime-y magic in use; copypasta FTW!).


With that in mind:

• Use @synchronized() if you like how it looks. The reality is that if your code is contending on that array, you probably have an architecture issue. Note: using @synchronized(someObject) may have unintended consequences in that it may cause additional contention if the object internally uses @synchronized(self)!

• Use dispatch_sync() with a serial queue if that is your thing. There is no overhead -- it is actually faster in both the contended and uncontended case -- and using queues are both easier to debug and easier to profile in that Instruments and the Debugger both have excellent tools for debugging queues (and they are getting better all the time) whereas debugging locks can be a pain.

• Use dispatch_async() with immutable data for heavily contended resources. I.e.:

- (void) addThing:(NSString*)thing { 
thing = [thing copy];
dispatch_async(_myQueue, ^{
[_myArray addObject:thing];
});
}

Finally, it shouldn't really matter which one you use for maintaining the contents of an array. The cost of contention is exceedingly high for the synchronous cases. For the asynchronous case, the cost of contention goes way down, but the potential for complexity or weird performance issues goes way up.

When designing concurrent systems, it is best to keep the boundary between queues as small as possible. A big part of that is ensuring that as few resources as possible "live" on both sides of a boundary.

@synchronized or serial dispatch queues

Certainly. There's also differences in semantics though. An @synchronized block uses a recursive, exception-safe mutex in a side-table. All of those properties lead to some additional overhead. dispatch_queue_t is extremely light weight (especially when using dispatch_*_f to avoid the Block_copy()), but is non-recursive, doesn't handle exceptions, and doesn't guarantee a particular thread.

Personally, I think recursive locks are a bad idea, and exceptions really shouldn't be caught in Cocoa apps, so @synchronized has very little appeal.

Future edit: the newer os_unfair_lock is even significantly lower overhead than either

Difference between DispatchQueue.main.async and DispatchQueue.main.sync

When you use async it lets the calling queue move on without waiting until the dispatched block is executed. On the contrary sync will make the calling queue stop and wait until the work you've dispatched in the block is done. Therefore sync is subject to lead to deadlocks. Try running DispatchQueue.main.sync from the main queue and the app will freeze because the calling queue will wait until the dispatched block is over but it won't be even able to start (because the queue is stopped and waiting)

When to use sync? When you need to wait for something done on a DIFFERENT queue and only then continue working on your current queue

Example of using sync:

On a serial queue you could use sync as a mutex in order to make sure that only one thread is able to perform the protected piece of code at the same time.

GCD vs @synchronized vs NSLock

There are many many details that could be discussed at great length in regards to this. But, at the core:

These always require a lock to be taken somewhere or somehow:

@synchronized(...) { ... }
[lock lock];

Locks are very expensive for the reasons you mention; they necessarily consume kernel resources. (The @synchronized() case actually may avoid kernel locks these days, but it is a hash based exclusion mechanism and that, in itself, is expensive).

And these do not always require a lock (but sometimes maybe do):

dispatch_sync(...concurrent q...., ^{ ... });
dispatch_async(...queue of any kind...., ^{ ... });

There is a fast path through the dispatch functions that are effectively lockless (though they will use test-and-set atomic primitives that can cause performance issues under load).

The end result is that a synchronous dispatch to a concurrent queue can effectively be treated as "execute this on this thread right now". A synchronous dispatch to a serial queue can do the atomic test-and-set to test if the queue is processing, mark it as busy, and, if it wasn't busy, execute the block on the calling thread immediately.

Asynchronous dispatches can be similarly as fast, though asynchronous dispatch requires copying the block (which can be very cheap, but something to think about).

In general, GCD can do anything a lock can do, can do it at least -- if not more -- efficiently, and you can use the GCD APIs to go way beyond just simple locking (using a semaphore as a computation throttle, for example).

BTW: If your tasks are relatively coarse grained, have a look at NSOperationQueue and NSOperation.

@synchronized (self) - best way

I assume you are updating some model objects asynchronously inside getDataWithCompletionBlock. Unfortunately, the code you posted will not synchronize asynchronous updates. Neither those updates, nor your completion block, itself, will be synchronized with that code.

Assuming you want to synchronize data that is asynchronously retrieved, you should:

  • remove the synchronized directive wrapping your method call;

  • don't update model objects inside the method;

  • instead, just retrieve it to a local variable which you then pass back in an additional parameter to the completion block;

  • then, in your completion block, perform the model update using the parameter passed to the block, synchronizing it as needed.

By the way, if you dispatch the updating of the model to the main queue, you may be able to retire synchronized directive entirely and instead use the main thread to synchronize all updates. (Dispatching updates and access to a serial queue is perfectly acceptable way to synchronize access from multiple threads, and main queue is, itself, a serial queue.) If all read and writes to the model take place on the main thread, that achieves the necessary synchronization. Obviously, though, if you're accessing the model from other threads, then you will need to synchronize it. But often restricting access to the model to the main thread is a nice, simple synchronization mechanism. As Apple says in their Concurrency Programming Guide:

Avoid Synchronization Altogether

For any new projects you work on, and even for existing projects, designing your code and data structures to avoid the need for synchronization is the best possible solution. Although locks and other synchronization tools are useful, they do impact the performance of any application. And if the overall design causes high contention among specific resources, your threads could be waiting even longer.

The best way to implement concurrency is to reduce the interactions and inter-dependencies between your concurrent tasks. If each task operates on its own private data set, it does not need to protect that data using locks. Even in situations where two tasks do share a common data set, you can look at ways of partitioning that set or providing each task with its own copy. Of course, copying data sets has its costs too, so you have to weigh those costs against the costs of synchronization before making your decision.

Anyway, if you wanted to minimize he need to synchronize yourself, it might look like:

[self getDataWithCompletionBlock:^(NSArray *results){
dispatch_async(dispatch_get_main_queue(), ^{
self.objects = results;
[refreshControl endRefreshing];
[self.collectionView reloadData];
loadingView.hidden = YES;
self.oneTimeCallReach = NO;
});
}];

Now, clearly, I don't know what your model is, so my NSArray example is probably not right, but hopefully this illustrates the idea. Let the completion handler take care of synchronized updates (and if you don't have any other threads accessing the model directly, then use main queue to synchronize access).

Personally, I might also include some error parameter so that the block that updates the UI can detect and handle any errors that might occur.

iOS Objective-c synchronized

Yes, with a caveat.

The @synchronized directive creates a mutex lock—preventing the code within the curly brackets from being executed by different threads at the same time. The caveat is that it uses the object that was passed to it as a unique identifier to distinguish the protected block. So if you're using @synchronized(self) in two different methods, those two methods are prevented from being executed by different threads at the same time (because they share the same identifier (in this case self)).

Objective-C: How do I modify the NSMutableArray in a different thread safely?

The easiest two ways to make things thread safe is either by using @synchronized or by dispatching things to a serial queue.

My strategy for maximum efficiency: Have a mutable and an immutable array. When you want to modify the array:

- (void)modifyArray {
@synchronized (self) {
if (_mutableArray == nil) {
_mutableArray = [(_immutableArray ?: @[]) mutableCopy];
_immutableArray = nil;
}
// Modify the mutable array.
}
}

- (NSArray*)immutableArray {
@synchronized (self) {
if (_mutableArray != nil) {
_immutableArray = [_mutableArray copy];
_mutableArray = nil;
}
return _immutableArray;
}
}

Obviously you would get the immutableArray once, have a call that erases the list, and then iterate through the immutableArray. Or have a method - (NSDictionary*)removeFirstElement which removes the first element and returns nil if there is nothing else to remove.

NSMutableArray collection and @Synchronized blocks

Yes, you have to synchronize read accesses to prevent them from happening simultaneously with mutations. Read accesses can safely run simultaneously with other read accesses, though.

If you have multiple readers, then it's worth investigating a read-write locking scheme. You can use pthread read-write locks (i.e. pthread_rwlock_...()).

Alternatively, you can use GCD on OS X 10.7+ and iOS 5+ with the "barrier" routines. Create a private concurrent queue. Submit all read operations to it in the normal fashion (e.g. dispatch_sync()). Submit mutation operations to it using a barrier routine such as dispatch_barrier_async(). (It can be asynchronous because you generally don't need to know that the mutation has completed before continuing. You only need to know that all reads submitted after you submit the mutation will see the results of the mutation, and the barrier guarantees that.)

You can learn more about this if you have access to the WWDC 2011 session video for Session 210 - Mastering Grand Central Dispatch.



Related Topics



Leave a reply



Submit