前言-死锁案例
// 在主线程中执行 dispatch_queue_t queueMain = dispatch_get_main_queue(); dispatch_sync(queueMain, ^{ NSLog(@"+++++++"); }); NSLog(@"hahahaha");
案例分析:运行结果是程序阻塞在dispatch_sync()处。由于main线程执行到dispatch_sync()处,线程处于等待状态。将block任务块添加到主串行队列最后,block等待当前任务(即正在主线程中执行的任务)执行完毕,而当前任务因为阻塞无法结束,导致两边都在等待,所以出现死锁。
一、简介
Dispatch queues是一种异步和并发执行任务的简单方式,将任务通过function或者block object的方式添加到dispatch queue。使用Dispatch queue还是使用thread?Dispatch queue中只需要封装任务,再将任务加到合适的Disptch queue,由系统去管理线程。如果用thread,需要做的工作就多了。使用Dispatch queue实现同步,实现效果比加锁要有效。
由dispatch queue管理提交的任务,所有任务遵从先进先出的规则,先添加到dispatch queue中的任务会最先开始执行。GCD提供了一些现成的dispatch queue,或者也可以自己定制dispatch queue。dispatch queue可分为3类:
1、Serial queues串行队列
serial queues(private dispatch queue)按dispatch queue中的顺序一个时间执行一个任务,常用于同步的获取指定资源。你可以随意创建serial queues,多个serial queues之间是并行的。换句话说,创建了4个serial queues,单个queue中的任务串行,queue与queue中的任务是并行的。
2、Concurrent queues并发队列
concurrent queues(global dispatch queue)并发的执行一个或多个任务,但也是按照先进先出的顺序开始启动任务。在某个时间点,可执行任务数量的上限取决于系统状态。在iOS5以后,可以通过函数dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)获取4中queue,也可以通过以下代码创建:
dispatch_queue_t queue = dispatch_queue_create("com.dispatch.concurrent", DISPATCH_QUEUE_CONCURRENT)
3、Main dispatch queue主队列
main dispatch queue(globally available serial queue)中的任务在主线程上串行执行。此queue和应用的run loop配合工作,主线程中在执行run loop的事件源后,插入queue任务。main dispatch queue为应用的主要同步queue。尽管不需要自己创建main dispatch queue,但使用时要注意。
二、与Dispatch Queues相关的技术
1、Dispatch groups
2、Dispatch semaphores
3、Dispatch sources(specific types of system events)
三、Block任务
1、block使用准则:
1)dispatch queue中的blocks异步地执行,最好在block只捕捉上下文中纯变量,不要试图捕捉结构体或者其它基于指针的变量,这些变量由调用上下文来分配和回收。原因是在block执行中,捕获的变量内存可能已经被回收。当然有解决方案,比如为该对象分配内存,然后blcok指向并retain当前内存。
2)dispatch queue会copy添加到其中的block。等任务结束再释放block。换句话说,在添加到queue时不需要明确写copy代码。
3)尽管在执行小任务时,queues调度执行任务比原始的threads更有效,但调度和执行block仍然有开销。如果block中的任务轻,直接执行反而更高效。
4)不要缓存与基础线程相关的数据,不要试图从其它的block中获取数据。如果希望同一个queue中的数据共享,使用dispatch queue的context指针来实现。
5)如果block内部创建了很多的OC对象,将block代码加到@autorelease块中,这样能更及时释放不再使用的对象。尽管dispatch queue中带有自动释放池,但不保证会及时回收那些OC对象。
四、创建和管理Dispatch Queue
1、获取Global Concurrent Dispatch Queues。
每个应用程序有4个global concurrent dispatch queues,他们之间只有优先级的区别,包括的优先级有:DISPATCH_QUEUE_PRIORITY_HIGHDISPATCH_QUEUE_PRIORITY_DEFAULT、DISPATCH_QUEUE_PRIORITY_LOW、DISPATCH_QUEUE_PRIORITY_BACKGROUND。尽管dispatch queue存在引用计数,但不需要retain/release该队列,因为他们在应用中是全局可访问的,只需要通过方法获取该queue即可,代码如下:
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
2、创建Serial Dispatch Queues
dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", NULL);
第一个参数是队列的名称,第二个暂时固定为NULL。
3、获取runtime的Common Queues
1)dispatch_get_current_queue()获取当前队列。如果在block内部调用,则返回的是block添加到的queue。如果是block外部调用,则返回当前queue。
2)dispatch_get_main_queue()获取主线程队列。
3)dispatch_get_global_queue()获取全局并发队列。
4、Dispatch Queue的内存管理
并发队列和主队列都不需要考虑内存管理的问题,只需要考虑自己定义的串行队列。通过dispatch_retain和dispatch_release方法增加和减少队列对象的引用计数器。
见到过在dealloc方法中,调用dispatch_release释放自定义的串行队列。
5、Dispatch Queue中数据存储-Context
dispatch queue能通过dispatch_set_context函数关联context data,通过dispatch_get_context方法来获取context data,context data存储了指向OC对象或者是数据结构的指针。context data需要手动分配和释放内存,可使用queue的finalizer函数完成释放。代码如下:
dispatch_queue_t storeData() { MyDataContext *contextData = (MyDataContext*) malloc(sizeof(MyDataContext)); myInitializeDataContextFunction(contextData); // 创建串行队列,设置context data dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL); dispatch_set_context(serialQueue, contextData); // 回收context data dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction); return serialQueue; } void myFinalizerFunction(void *context) { MyDataContext* contextData = (MyDataContext*)context; // 清理内容结构 myCleanUpDataContextFunction(contextData); // 释放 free(contextData);
}
五、往Dispatch Queues中添加任务
1、添加单一任务
dispatch_sync()同步调用,会阻塞方法所在线程。
dispatch_async()异步调用,不会阻塞线程。
dispatch_barrier_async()。等待前面的任务全部结束,该task才能执行,然后后面的任务才能启动执行
dispatch_once(&onceToken,^{}); // static dispatch_once_t onceToken;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2.0*NSEC_PER_SEC)),dispatch_get_main_queue(), ^{});// 将2秒后需执行的任务......
2、Completion Block
在dispatch queue中的某个任务执行结束后,通知做些后续操作。不同于回调机制,dispatch queue采用completion block实现。具体操作是,在其任务最后添加将completion block提交到指定queue中。
// myBlock和myQueue分别就是completion block和提交至的queue void sum_async(int data01, int data02, dispatch_queue_t myQueue, void (^myBlock)(int)) { // Retain the queue provided by the user to make sure it does not disappear before the completion block can be called. dispatch_retain(myQueue); // Do the work on the default concurrent queue, then call the user-provided block with the results. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ int sumValue = data + len; dispatch_async(myQueue, ^{ myBlock(sumValue); }); // Release the user-provided queue when done dispatch_release(myQueue); }); }
3、并发执行循环迭代dispatch_apply()
在执行指定次数的循环中,且单个循环间彼此独立、执行的先后顺序无关紧要。这时不应该再采用最初的for循环依次遍历,而应考虑通过concurrent dispatch queue提高性能。有个函数dispatch_apply(),能一次性的将所有循环提交给queue,当提交给concurrent queue时,就能同一时间执行多个循环。
当然也可以将任务提交到serial queue,但没什么优势和意义,不建议。
注意:跟普通的循环一样,dispatch_apply函数需等所有的循环运行结束后才会返回。因为会堵塞当前线程,在主线程中要慎用,避免执行所有循环阻碍主线程及时响应事件。如果循环要求比较长的处理时间,你应该在其它线程上调用dispatch_apply。
避免出现死锁场景:执行dispatch_apply所在线程的queue和入参中的queue是同一个,且queue是serial queue,那么线程执行到dispatch_apply函数时,需等待所有循环运行结束,而serial queue中的任务又只执行到dispatch_apply,得等当前任务执行完毕,双方互相等待彼此结束任务。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // count是总共循环次数,i是当前循环数 dispatch_apply(count, queue, ^(size_t i) { printf("%u ",i); });
4、在主线程上执行任务
GCD提供的一个特殊的main dispatch queue,其中的任务在主线程中串行。可通过dispatch_get_main_queue函数可获取该queue,它由应用自动提供且自动回收,给主线程创建了一个run loop。
如果创建的不是cocoa应用程序,也不想明确创建run loop,那必须调用dispatch_main函数去明确执行main dispatch queue中的任务,否则,queue中的任务永远不会执行。
5、在任务中使用OC对象
GCD建立在cocoa内存管理的基础上,可以在任务中任意使用OC对象,每一个dispatch queue都拥有自己的autorelease pool。但如果应用内存要求严格,且在任务中添加了很多的autoreleased对象,想要及时释放这些对象,创建自己的autorelease pool。
六、暂停和恢复Queue
通过调用dispatch_suspend函数暂时停止queue中的任务执行,调用dispatch_resume函数恢复执行。有一个suspension reference count,当暂停时该引用计数增加,当恢复时引用计数减少,为0时,queue是暂停状态。因此,必须平衡调用suspend和resume方法。
注意:这两个方法都是异步执行,在queue中任务之间生效,也就是说暂停不会让正在执行的任务中止,中止是下一个任务。
7、使用Dispatch Semaphore来有效使用有限资源
给某个有限资源,指定能同时访问的最大数量,可以通过dispatch semaphore实现。dispatch semaphore跟大多数semaphore一样,除了获取dispatch semaphore比系统的semaphore要快。这是因为GCD一般不访问内核,只有在资源不可用时,才会去内核调用,系统暂停线程以待semaphore发出信号。使用如下:
1、通过dispatch_semaphore_create函数创建semaphore,可以选择指定一个正整数来显示一次可访问上限。
2、在每个任务,通过dispatch_semaphore_wait函数等待信号量。
3、当信号量发出信号,就能获取资源执行任务。
4、当使用资源结束后,通过dispatch_semaphore_signal函数释放信号并发出信号。
这些步骤如何工作可参考系统的file descriptor。每个应用提供了有限的file descriptors,如果某个任务是处理大量的文件,你不想一次打开这么多文件以至于耗尽file descriptors,这时,就可以采用semaphore去限制使用file descriptors的数量,使用部分代码如下:
// 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);
八、Group Queue组任务队列
dispatch group阻塞线程直到一个或多个任务执行结束。可用的场景是等待所有指定任务完成后,再执行某任务。另一个类似于使用dispatch group的是thread join,两者实现方式不同。
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和线程安全
任何时候实现并发,都需要知道以下几点:
1、dispatch queue本身是线程安全的,换句话说,可以从任何线程提交任务到queue中,不需要考虑queue的锁和同步问题。
2、dispatch_sync调用所在的queueA,与入参queueB,如果queueA和queueB是同一个queue,那么会造成死锁的情况。使用dispatch_async函数就不会。
3、避免在任务中使用锁。如果是串行队列,如果获取不到锁,会阻塞整个队列中的任务执行。如果是并行队列,获取不到锁,会阻塞队列中其它任务的执行。所以需要同步执行代码,使用serial dispatch queue代替锁。
4、尽管可以获取任务所在基础线程信息,最好不要这样做。
十、该博客翻译自苹果官方文档
https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW8