Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mpw/6892d39e48080815979f2a7bc9648a06 to your computer and use it in GitHub Desktop.
Save mpw/6892d39e48080815979f2a7bc9648a06 to your computer and use it in GitHub Desktop.
Making efficient use of the libdispatch (GCD)

libdispatch efficiency tips

I suspect most developers are using the libdispatch inefficiently due to the way it was presented to us at the time it was introduced and for many years after that, and due to the confusing documentation and API. I realized this after reading the 'concurrency' discussion on the swift-evolution mailing-list, in particular the messages from Pierre Habouzit (who is the libdispatch maintainer at Apple) are quite enlightening (and you can also find some good tweets from him on the subject).

My take-aways are:

  • You should have very few queues that target the global pool. If all these queues are active at once, you will get as many threads running. These queues should be seen as execution contexts in the program (gui, storage, background work, ...) that benefit from executing in parallel. Start with few of them, reuse queues by default and add more if there's some measurable benefit to it. In most apps, you probably need less than 5.

  • Queues that target other queues are fine, these are the ones which scale.

  • Don't use dispatch_get_global_queue(), it doesn't play nice with qos/priorities and can lead to thread explosion. Run your code on one of your execution context instead.

  • dispatch_async() is wasteful if the dispatched block is small, as it will most likely require a new thread due to libdispatch's overcommit behavior. Prefer locking to protect shared state (rather than switching the execution context).

  • Some classes/libraries are better designed as reusing the execution context from their callers/clients. That means using traditional locking for thread-safety. Note that locking can be achieved via a dispatch queue and making only dispatch_sync() calls to it. Also note that unfair_lock seems to be the new hot thing to be using now (nicer with priorities, less context switches).

  • You don't need to be async all the way to avoid thread explosion. Using a limited number of bottom queues and not using dispatch_get_global_queue() is the better fix for this.

  • Concurrent queues are not as optimized as serial queues. Use them if you measure a performance improvement, otherwise it's likely premature optimization.

  • libdispatch is efficient but not magic. Resources are not infinite. You cannot ignore the reality of the underlying operating system and hardware you're running on. Not all code is prone to parallelization.

You need to think whether the work you're dispatching is worth switching to a different execution context. Most of the time, locking is probably the better choice. Also I personally think it's not worth dealing with the complexity (and bugs) of heavy async designs if you don't get anything out of it, synchronous code is a lot more readable and maintainable.

Once you start to have well defined queues (execution contexts) and to reuse them, you may run into deadlocks if you dispatch_sync() to them. This usually happens when queues are used for thread-safety, again the solution is locking instead and using dispatch_async() when you need to switch to another execution context.

I've personally seen massive performance improvements by following these recommandations (on a high throughput program). It's a new way of doing things but it's worth it.

@tclementdev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment