iOS并发编程
iOS和OS X使用异步设计方法(asynchronous design approach)去解决并发的问题,而不是直接使用线程。以下是两种解决并发执行的方法:
Grand Central Dispatch (GCD)
GCD帮组管理线程代码并把这部分代码移到系统层次。开发者需要做的只是定义需要完成的任务然后把其放到合适的dispatch queue(下面简写为DQ)中。GCD负责创建必须的线程和在这些线程上调度任务。
Operation queues
Operation Queues(下面简写为OQ)是Obj-C的对象,工作原理基本和DQ一样。所有的线程管理都由OQ管理。
一些重要概念的简单解释:
Dispatch Queues
DQ是执行自定义任务的基于C的机制。DQ不管是串行还是并行,都是FIFO的。
Dispatch Sources
dispatch source是用于处理系统各种特定类型事件的机制。DS封装一些必要的信息,例如系统事件的类型,并会在事件发生的时候,向DQ提交一个特定的block或者函数。
系统事件包括:
-
Timers
-
Signal handlers
-
Descriptor-related events
-
Process-related events
-
Mach port events
-
Custom events that you trigger
Operation Queues
OQ是在Cocoa框架中等价于并发dispatch queue的东西,它实现NSOperationQueue。相对于DQ按FIFO的顺序执行任务,OQ引入其他的因素决定执行顺序。最原始的一个因素是任务的执行是否受另外一些任务执行完成的影响。在定义任务的时候配置这些因素并且使用它们去创建一个复杂的执行顺序路径。
Tips for Improving Efficiency
In addition to simply factoring your code into smaller tasks and adding them to a queue, there are other ways to improve the overall efficiency of your code using queues:
-
Consider computing values directly within your task if memory usage is a factor. If your application is already memory bound, computing values directly now may be faster than loading cached values from main memory. Computing values directly uses the registers and caches of the given processor core, which are much faster than main memory. Of course, you should only do this if testing indicates this is a performance win.
-
Identify serial tasks early and do what you can to make them more concurrent. If a task must be executed serially because it relies on some shared resource, consider changing your architecture to remove that shared resource. You might consider making copies of the resource for each client that needs one or eliminate the resource altogether.
-
Avoid using locks. The support provided by dispatch queues and operation queues makes locks unnecessary in most situations. Instead of using locks to protect some shared resource, designate a serial queue (or use operation object dependencies) to execute tasks in the correct order.
-
Rely on the system frameworks whenever possible. The best way to achieve concurrency is to take advantage of the built-in concurrency provided by the system frameworks. Many frameworks use threads and other technologies internally to implement concurrent behaviors. When defining your tasks, look to see if an existing framework defines a function or method that does exactly what you want and does so concurrently. Using that API may save you effort and is more likely to give you the maximum concurrency possible.
Dispatch Queues
GCD自动提供一些DQ,当然也可以根据需求创建必须的自定义的。
Type |
Description |
---|---|
Serial |
Serial queues (also known as private dispatch queues) execute one task at a time in the order in which they are added to the queue. The currently executing task runs on a distinct thread (which can vary from task to task) that is managed by the dispatch queue. Serial queues are often used to synchronize access to a specific resource. You can create as many serial queues as you need, and each queue operates concurrently with respect to all other queues. In other words, if you create four serial queues, each queue executes only one task at a time but up to four tasks could still execute concurrently, one from each queue. For information on how to create serial queues, see “Creating Serial Dispatch Queues.” |
Concurrent |
Concurrent queues (also known as a type of global dispatch queue) execute one or more tasks concurrently, but tasks are still started in the order in which they were added to the queue. The currently executing tasks run on distinct threads that are managed by the dispatch queue. The exact number of tasks executing at any given point is variable and depends on system conditions. In iOS 5 and later, you can create concurrent dispatch queues yourself by specifying |
Main dispatch queue |
The main dispatch queue is a globally available serial queue that executes tasks on the application’s main thread. This queue works with the application’s run loop (if one is present) to interleave the execution of queued tasks with the execution of other event sources attached to the run loop. Because it runs on your application’s main thread, the main queue is often used as a key synchronization point for an application. Although you do not need to create the main dispatch queue, you do need to make sure your application drains it appropriately. For more information on how this queue is managed, see “Performing Tasks on the Main Thread.” |
DQ的好处是让你更多关注的是执行的任务,而不是去关心线程的创建和管理。
如果有两个在不同线程的任务需要访问一个共享的资源,如果使用线程需要使用锁去实现这个操作。如果使用DQ,可以把任务放到serial dispatch queue中去,保证一个时刻只有一个任务在访问资源。这相对于使用锁去同步,有很多的性能优势。锁请求昂贵的kernel trap不管是否抢占资源,但是DQ工作于用户进程空间,只有在必须的时候才会向内核调用。
对于运行在串行队列的任务,并不是并发的,但相对于使用lock的情况,线程的并发优势会大大降低甚至没有了,更重要的是,线程的创建消耗内核和用户空间的内存。DQ不用为它们的线程牺牲同样的内存,而且DQ使用的线程会保持繁忙并且是不阻塞的。
DQ的一些关键点:
-
Dispatch queues execute their tasks concurrently with respect to other dispatch queues. The serialization of tasks is limited to the tasks in a single dispatch queue.多DQ之间任务是并发的,单DQ内任务是串行的。
-
The system determines the total number of tasks executing at any one time. Thus, an application with 100 tasks in 100 different queues may not execute all of those tasks concurrently (unless it has 100 or more effective cores).
-
The system takes queue priority levels into account when choosing which new tasks to start. For information about how to set the priority of a serial queue, see “Providing a Clean Up Function For a Queue.”
-
Tasks in a queue must be ready to execute at the time they are added to the queue. (If you have used Cocoa operation objects before, notice that this behavior differs from the model operations use.)
-
Private dispatch queues are reference-counted objects. In addition to retaining the queue in your own code, be aware that dispatch sources can also be attached to a queue and also increment its retain count. Thus, you must make sure that all dispatch sources are canceled and all retain calls are balanced with an appropriate release call. For more information about retaining and releasing queues, see“Memory Management for Dispatch Queues.” For more information about dispatch sources, see “About Dispatch Sources.”
除了DQ外,GCD提供以下技术使用queue去管理代码:
Technology |
Description |
---|---|
Dispatch groups |
A dispatch group is a way to monitor a set ofblock objects for completion. (You can monitor the blocks synchronously or asynchronously depending on your needs.) Groups provide a useful synchronization mechanism for code that depends on the completion of other tasks. For more information about using groups, see “Waiting on Groups of Queued Tasks.” |
Dispatch semaphores |
A dispatch semaphore is similar to a traditional semaphore but is generally more efficient. Dispatch semaphores call down to the kernel only when the calling thread needs to be blocked because the semaphore is unavailable. If the semaphore is available, no kernel call is made. For an example of how to use dispatch semaphores, see “Using Dispatch Semaphores to Regulate the Use of Finite Resources.” |
Dispatch sources |
A dispatch source generates notifications in response to specific types of system events. You can use dispatch sources to monitor events such as process notifications, signals, and descriptor events among others. When an event occurs, the dispatch source submits your task code asynchronously to the specified dispatch queue for processing. For more information about creating and using dispatch sources, see “Dispatch Sources.” |
Block使用的一些原则:
-
For blocks that you plan to perform asynchronously using a dispatch queue, it is safe to capture scalar variables from the parent function or method and use them in the block. However, you should not try to capture large structures or other pointer-based variables that are allocated and deleted by the calling context. By the time your block is executed, the memory referenced by that pointer may be gone. Of course, it is safe to allocate memory (or an object) yourself and explicitly hand off ownership of that memory to the block.对于block,使用父方法的数量值是安全的,不要使用calling context分配和删除的大的结构体或者指针。当block执行的时候,那些指针指向的内存可能已经不存在了。
-
Dispatch queues copy blocks that are added to them, and they release blocks when they finish executing. In other words, you do not need to explicitly copy blocks before adding them to a queue.DQ会复制添加到它们的block。
-
Although queues are more efficient than raw threads at executing small tasks, there is still overhead to creating blocks and executing them on a queue. If a block does too little work, it may be cheaper to execute it inline than dispatch it to a queue. The way to tell if a block is doing too little work is to gather metrics for each path using the performance tools and compare them.不要让block执行开销比dispatch它到queue上还少
-
Do not cache data relative to the underlying thread and expect that data to be accessible from a different block. If tasks in the same queue need to share data, use the context pointer of the dispatch queue to store the data instead. For more information on how to access the context data of a dispatch queue, see “Storing Custom Context Information with a Queue.”
使用DQ的上下文指针(context pointer)去保存block之间的共享数据,而不是在一个block中缓存它并在另一个block中访问
-
If your block creates more than a few Objective-C objects, you might want to enclose parts of your block’s code in an @autorelease block to handle the memory management for those objects. Although GCD dispatch queues have their own autorelease pools, they make no guarantees as to when those pools are drained. If your application is memory constrained, creating your own autorelease pool allows you to free up the memory for autoreleased objects at more regular intervals.
如果在block中创建大量的Obj-C对象,请使用@autorelease
Creating and Managing Dispatch Queues
Getting the Global Concurrent Dispatch Queues
系统提供四条并发DQ。这四条DQ对于应用是全局的,它们之间的区别是优先级。
Creating Serial Dispatch Queues
相对于并发DQ,串行DQ是要自己显式创建和管理的。
Storing Custom Context Information with a Queue
所有的dispatch对象运行你关联自定义的上下文数据到该对象上。使用方法:
系统不会使用这些自定义的数据,这些数据的分配和析构都取决于你。
Providing a Clean Up Function For a Queue
创建串行DQ可以指定回收函数在queue被析构的时候,去做自定义的清理工作。
Listing 3-3 Installing a queue clean up function
void myFinalizerFunction(void *context) |
{ |
MyDataContext* theData = (MyDataContext*)context; |
// Clean up the contents of the structure |
myCleanUpDataContextFunction(theData); |
// Now release the structure itself. |
free(theData); |
} |
dispatch_queue_t createMyQueue() |
{ |
MyDataContext* data = (MyDataContext*) malloc(sizeof(MyDataContext)); |
myInitializeDataContextFunction(data); |
// Create the queue and set the context data. |
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL); |
if (serialQueue) |
{ |
dispatch_set_context(serialQueue, data); |
dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction); |
} |
return serialQueue; |
} |
Adding Tasks to a Queue
Important: You should never call the dispatch_sync
ordispatch_sync_f
function from a task that is executing in the same queue that you are planning to pass to the function. This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.
不要在执行中的任务中调用
dispatch_sync
和 dispatch_sync_f,而且传递给
dispatch_sync
和 dispatch_sync_f的queue参数和这个任务的queue是同一个。
dispatch_queue_t myCustomQueue; |
myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL); |
dispatch_async(myCustomQueue, ^{ |
printf("Do some work here.\n"); |
}); |
printf("The first block may or may not have run.\n"); |
dispatch_sync(myCustomQueue, ^{ |
printf("Do some more work here.\n"); |
}); |
printf("Both blocks have completed.\n"); |
Performing a Completion Block When a Task Is Done
被dispatch到queue的任务是与创建它的代码是独立运行的。很多时候,需要在任务执行完成后,执行一个回调。在DQ中,可以使用completion block去取代传统的回调方法。completion block就是需要dispatch到queue中的一段代码。
void average_async(int *data, size_t len, |
dispatch_queue_t queue, void (^block)(int)) |
{ |
// Retain the queue provided by the user to make |
// sure it does not disappear before the completion |
// block can be called. |
dispatch_retain(queue); |
// Do the work on the default concurrent queue and then |
// call the user-provided block with the results. |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
int avg = average(data, len); |
dispatch_async(queue, ^{ block(avg);}); |
// Release the user-provided queue when done |
dispatch_release(queue); |
}); |
} |
Performing Loop Iterations Concurrently
对于需要执行循环并发的情况,可以使用dispatch_apply
或者 dispatch_apply_f。这适用于执行顺序是不重要的情况。对于串行队列,使用这方法对性能是没有提升的。
Important: Like a regular for
loop, the dispatch_apply
anddispatch_apply_f
functions do not return until all loop iterations are complete. You should therefore be careful when calling them from code that is already executing from the context of a queue. If the queue you pass as a parameter to the function is a serial queue and is the same one executing the current code, calling these functions will deadlock the queue.
Because they effectively block the current thread, you should also be careful when calling these functions from your main thread, where they could prevent your event handling loop from responding to events in a timely manner. If your loop code requires a noticeable amount of processing time, you might want to call these functions from a different thread.
和普通for循环一样,dispatch_apply和dispatch_apply_f会在所有的循环结束之后才会返回。所以,如果传递给dispatch_apply和dispatch_apply_f的queue参数是当前任务运行着的queue,而且这个queue是一个串行queue,那么这个调用这个方法就会导致queue死锁。
Using Objective-C Objects in Your Tasks
每一个dispatch queue持有自己的的autorelease pool。
Using Dispatch Semaphores to Regulate the Use of Finite Resources
在访问一些有限的资源时,就需要用到dispatch semaphores去控制这些任务的数量。dispatch semaphores的好处时减少内核调用,只有在资源不足时才会内核调用并停止线程,知道信号量被标记。
使用dispatch semaphores的顺序:
-
When you create the semaphore (using the
dispatch_semaphore_create
function), you can specify a positive integer indicating the number of resources available. -
In each task, call
dispatch_semaphore_wait
to wait on the semaphore. -
When the wait call returns, acquire the resource and do your work.
-
When you are done with the resource, release it and signal the semaphore by calling the
dispatch_semaphore_signal
function.
// Create the semaphore, specifying the initial pool size |
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2); |
// Wait for a free file descriptor |
dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER); |
fd = open("/etc/services", O_RDONLY); |
// Release the file descriptor when done |
close(fd); |
dispatch_semaphore_signal(fd_sema); |
dispatch_semaphore_wait会时时资源数减1,如果为负数,通知内核阻塞线程。
Waiting on Groups of Queued Tasks
dispatch group的作用时在某些任何执行完成之前阻塞线程。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
dispatch_group_t group = dispatch_group_create(); |
// Add a task to the group |
dispatch_group_async(group, queue, ^{ |
// Some asynchronous work |
}); |
// Do some other work while the tasks execute. |
// When you cannot make any more forward progress, |
// wait on the group to block the current thread. |
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); |
// Release the group when it is no longer needed. |
dispatch_release(group); |
Dispatch Queues and Thread Safety
对于线程安全的一些Tips:
-
Dispatch queues themselves are thread safe. In other words, you can submit tasks to a dispatch queue from any thread on the system without first taking a lock or synchronizing access to the queue.
-
Do not call the
dispatch_sync
function from a task that is executing on the same queue that you pass to your function call. Doing so will deadlock the queue. If you need to dispatch to the current queue, do so asynchronously using thedispatch_async
function. -
Avoid taking locks from the tasks you submit to a dispatch queue. Although it is safe to use locks from your tasks, when you acquire the lock, you risk blocking a serial queue entirely if that lock is unavailable. Similarly, for concurrent queues, waiting on a lock might prevent other tasks from executing instead. If you need to synchronize parts of your code, use a serial dispatch queue instead of a lock.
-
Although you can obtain information about the underlying thread running a task, it is better to avoid doing so. For more information about the compatibility of dispatch queues with threads, see“Compatibility with POSIX Threads.”
Dispatch Sources
与底层系统交互往往需要耗费大量的时间。内核调用或者其他系统底层调用会导致上下文的改变,这会比在自己进程中的调用有更多的开销。所以很多系统库会提供异步接口去允许程序向系统异步提交请求。
GCD使用blcok和DQ向系统提交请求并得到相应的结果返回。
About Dispatch Sources
dispatch sources是协调系统底层事件处理的基础数据结构。GCD支持以下的类型的dispatch sources:
-
Timer dispatch sources generate periodic notifications.
-
Signal dispatch sources notify you when a UNIX signal arrives.
-
Descriptor sources notify you of various file- and socket-based operations, such as:
-
When data is available for reading
-
When it is possible to write data
-
When files are deleted, moved, or renamed in the file system
-
When file meta information changes
-
-
Process dispatch sources notify you of process-related events, such as:
-
When a process exits
-
When a process issues a
fork
orexec
type of call -
When a signal is delivered to the process
-
-
Mach port dispatch sources notify you of Mach-related events.
-
Custom dispatch sources are ones you define and trigger yourself.
dispatch source取代异步回调方法,实现响应相关的系统事件。配置dispatch source时,需要指定监听的事件,DQ和用于处理事件的代码。当关心的事件发生时,dispatch source会向指定的DQ提交block或者函数。
dispatch source会retain相关联的DQ防止其被释放。
为防止DQ事件积压,dispatch sources实现了事件合并方案。如果在旧事件没有被处理或者执行前,新事件到达了,dispatch source会合并两个事件的数据。根据事件类型,可能会取代旧事件或者更新旧事件中的数据。
Creating Dispatch Sources
创建DS的步骤:
-
Create the dispatch source using the
dispatch_source_create
function. -
Configure the dispatch source:
-
Assign an event handler to the dispatch source; see “Writing and Installing an Event Handler.”
-
For timer sources, set the timer information using the
dispatch_source_set_timer
function; see “Creating a Timer.”
-
-
Optionally assign a cancellation handler to the dispatch source; see “Installing a Cancellation Handler.”
-
Call the
dispatch_resume
function to start processing events; see “Suspending and Resuming Dispatch Sources.”
Writing and Installing an Event Handler
使用dispatch_source_set_event_handler
或者dispatch_source_set_event_handler_f 去设置事件handler。当事件发生,事件handler会被提交到DQ中等待处理。
两种类型的handler:
// Block-based event handler |
void (^dispatch_block_t)(void) |
// Function-based event handler |
void (*dispatch_function_t)(void *) |
在事件handler内部,可以从DS中获取关于事件的相关信息。
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, |
myDescriptor, 0, myQueue); |
dispatch_source_set_event_handler(source, ^{ |
// Get some data from the source variable, which is captured |
// from the parent context. |
size_t estimated = dispatch_source_get_data(source); |
// Continue reading the descriptor... |
}); |
dispatch_resume(source); |
事件handler中获取到的变量默认是只读的。虽然block特性支持修改这些变量在特定的情况下,但是不要尝试这样做。DS是异步执行事件handler的,所以内部获取的变量可能在执行的时候已经不存在。
以下方法,可以获取事件的信息:
Function |
Description |
---|---|
This function returns the underlying system data type that the dispatch source manages. For a descriptor dispatch source, this function returns an For a signal dispatch source, this function returns an For a process dispatch source, this function returns a For a Mach port dispatch source, this function returns a For other dispatch sources, the value returned by this function is undefined. |
|
This function returns any pending data associated with the event. For a descriptor dispatch source that reads data from a file, this function returns the number of bytes available for reading. For a descriptor dispatch source that writes data to a file, this function returns a positive integer if space is available for writing. For a descriptor dispatch source that monitors file system activity, this function returns a constant indicating the type of event that occurred. For a list of constants, see the For a process dispatch source, this function returns a constant indicating the type of event that occurred. For a list of constants, see the For a Mach port dispatch source, this function returns a constant indicating the type of event that occurred. For a list of constants, see the For a custom dispatch source, this function returns the new data value created from the existing data and the new data passed to the |
|
This function returns the event flags that were used to create the dispatch source. For a process dispatch source, this function returns a mask of the events that the dispatch source receives. For a list of constants, see the For a Mach port dispatch source with send rights, this function returns a mask of the desired events. For a list of constants, see the For a custom OR dispatch source, this function returns the mask used to merge the data values. |
Installing a Cancellation Handler
Cancellation handler的作用是用于在DS被release前清楚资源。多少类型的DS,Cancellation handler是可选实现的,只有在对需要被更新的DS执行一些自定义行为时需要到。对于使用descriptor或者Mach port的DS而言,必须提供Cancellation handler去关闭它们。
可以通过 dispatch_source_set_cancel_handler
ordispatch_source_set_cancel_handler_f
设置cancellation handler。
Changing the Target Queue
可以在任何时候使用dispatch_set_target_queue
函数去修改创建DS时指定的queue。在需要改变DS的事件的优先级的时候,需要做这个操作。
这个操作时异步的,并不能保证即时执行。对于已经出列的在等待处理的事件handler,它会继续在前一个queue里面执行。
Associating Custom Data with a Dispatch Source
可以使用dispatch_set_context函数去关联自定义的数据到DS上。可以使用上文件指针去存储存储数据,如果存储了,旧必须要设置cancellation handler去释放它。
Canceling a Dispatch Source
DS一直处于活动状态知道显式调用 dispatch_source_cancel
函数,cancel后还要清除掉它。
void RemoveDispatchSource(dispatch_source_t mySource) |
{ |
dispatch_source_cancel(mySource); |
dispatch_release(mySource); |
} |
Suspending and Resuming Dispatch Sources
可以使用dispatch_suspend
和 dispatch_resume去暂停和重启DS。