A few weeks ago, I recommended an article about dispatch queues and then demonstrated how to create a simple asynchronous dispatch queue using C++11.
Continuing on the theme of "threads are dangerous", today's reading material is again pulled from Apple's website: Migrating Away from Threads. Apple provides various strategies and examples for developers looking to migrate from threads to a dispatch model. If you're interested in utilizing dispatch queues in your system, I highly recommend reading this article.
Specifically, using dispatch queues and operation queues instead of threads has several advantages:
- It reduces the memory penalty your application pays for storing thread stacks in the application’s memory space.
- It eliminates the code needed to create and configure your threads.
- It eliminates the code needed to manage and schedule work on threads.
- It simplifies the code you have to write.
We often will use these basic thread models in our code. These models are essentially the same, and simply differ in how we choose to manage the thread.
- Single task threads. Create a thread to perform a single task and release the thread when the task is done.
- Worker threads. Create one or more worker threads with specific tasks in mind for each. Dispatch tasks to each thread periodically.
- Thread pools. Create a pool of generic threads and set up run loops for each one. When you have a task to perform, grab a thread from the pool and dispatch the task to it. If there are no free threads, queue the task and wait for a thread to become available.
For tasks that are not particularly contentious—that is, tasks that do not take locks—you should be able to make the following direct replacements:
- For a single task thread, encapsulate the task in a block or operation object and submit it to a concurrent queue.
- For worker threads, you need to decide whether to use a serial queue or a concurrent queue. If you use worker threads to synchronize the execution of specific sets of tasks, use a serial queue. If you do use worker threads to execute arbitrary tasks with no interdependencies, use a concurrent queue.
- For thread pools, encapsulate your tasks in a block or operation object and dispatch them to a concurrent queue for execution.
Instead of using locks, you can use queues to perform many of the same tasks:
- If you have tasks that must execute in a specific order, submit them to a serial dispatch queue. If you prefer to use operation queues, use operation object dependencies to ensure that those objects execute in a specific order.
- If you are currently using locks to protect a shared resource, create a serial queue to execute any tasks that modify that resource. The serial queue then replaces your existing locks as the synchronization mechanism
However, dispatch queues do have limitations:
The asynchronous programming model offered by queues is appropriate in situations where latency is not an issue. Even though queues offer ways to configure the execution priority of tasks in the queue, higher execution priorities do not guarantee the execution of tasks at specific times. Therefore, threads are still a more appropriate choice in cases where you need minimal latency, such as in audio and video playback.
Ways that queues are better than locks:
Instead of using a lock to protect a shared resource, you can instead create a queue to serialize the tasks that access that resource. Queues do not impose the same penalties as locks. For example, queueing a task does not require trapping into the kernel to acquire a mutex.
What's the difference between synchronous and asynchronous?
Submitting a task asynchronously lets the current thread continue to run while the task is performed. Submitting a task synchronously blocks the current thread until the task is completed. Both options have appropriate uses, although it is certainly advantageous to submit tasks asynchronously whenever you can.