effective OC2.0 52阅读笔记(六 大中枢派发)+ Objective-C高级编程 (三Grand Central Dispatch)
Grand Central Dispatch (自动管理线程的生命周期(创建,调度任务,销毁))
---------------------------------------------
(是否开启新线程:Y/N 并行:B 串行:C)
全局并发 串行 主队列
同步 N/C N/C N/C
异步 Y/B Y/C N/C
同步函数不具备开启线程的能力。无论何种队列。异步函数具备开启新线程的能力。开启几条线程由队列决定。
注意:串行队列并不一定是对应一个线程的,将异步任务添加到串行队列中也会开启新线程。
3.1.1概要:
通过GCD提供的系统级线程管理提高执行效率。
3.1.2多线程编程:
由于一个CPU只能执行一个命令,不能执行某处分开并列的两个命令。1个CPU执行的CPU命令列为一条无分叉路经,即为线程。如果该无分叉路径不止一条,存在多条即为多线程。(这里还有一个并发和并行的概念,并发:线程之间的上下文切换;并行:真正多核并行)
多线程编程优点:保证应用程序的响应性能。
多线程编程缺点:可能会导致数据竞争,死锁,太多线程会消耗大量内存。
3.2 GCD的API
3.2.1 Dispatch Queue
Dispatch Queue 种类:串行队列Serial Dispatch Queue等待现在执行中处理结束,使用一个线程;并行队列Concurrent Dispatch Queue不等待现在执行中处理结束,使用多个线程。但XNU内核决定应当使用的线程数。
创建:
方法一:dispatch_queue_create
串行:当生成多个Serial Dispatch Queue时(此时会生成多个线程),各个Serial Dispatch Queue将并行执行。
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue",NULL);
并行:
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.myConcurrentDispatchQueue",DISPATCH_QUEUE_CONCURRENT);
多个线程更新相同资源导致数据竞争时,使用串行队列。其它情况处理使用并行队列。
注意:由create生成的Dispatch Queue需要由程序员进行释放。因为Dispatch Queue并没有像Block那样具有作为OC对象来处理的技术。释放:dispatch_release(),retain:dispatch_retain()。(注意这里在iOS10.8和iOS6.0前需要手动释放,但是之后已经不需要手动release了,如果调用该方法,会引起编译器错误)。
举例:
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.myConcurrentDispatchQueue",DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myConcurrentDispatchQueue, ^{
NSLog(@"hhhhhhhh");
});
dispatch_release(myConcurrentDispatchQueue);//现在已不需要
方法二:Main Dispatch Queue / Global Dispatch Queue (在之前的系统中也不需要retain和release)
Main Dispatch Queue:在主线程中执行的Dispatch Queue,且是Serial Dispatch Queue。追加到Main Dispatch Queue的处理会在主线程的RunLoop中执行。
Global Dispatch Queue:是所有程序都能使用的Concurrent Dispatch Queue。只要获取Global Dispatch Queue使用即可。有四个执行优先级 High Priority,Default Priority,Low Priority,Background Priority。
对create生成的队列设置优先级:
默认create生成的优先级都是默认优先级。变更需要使用dispatch_set_target_queue。第一个参数为create生成的队列,第二个参数没限制。
执行层次:可通过设置多个Serial Dispatch Queue的第二个参数为某一个Serial Dispatch Queue。使得原想并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue上只能同时执行一个处理。
(见 dispatch_get_current_queue)
延迟执行:dispatch_after
注意此函数并不是在指定时间后执行处理,而是在指定时间后追加处理到Disptach Queue。
//dispatch_time 通常用于计算相对时间,其中NSEC_PER_SEC为秒,NSEC_PER_MSEC为毫秒
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"最快3秒后执行,最慢3+1/60秒后执行");
});//相当于dispatch_async函数追加Block到MainDispatchQueue
//dispatch_walltime函数用于计算绝对时间,可在有闹钟的功能中使用
dispatch_time_t getDispatchTimeByDate(NSDate *date)
{
NSTimeInterval interval = [date timeIntervalSince1970];
double second,subsecond;
subsecond = modf(interval, &second);
//函数原型:double modf(double x, double *ipart) 函数用途:分解x,以得到x的整数和小数部分
struct timespec time;
time.tv_sec = second;
time.tv_nsec = subsecond * NSEC_PER_SEC;
dispatch_time_t milestone = dispatch_walltime(&time, 0);
return milestone;
}
队列组:Dispatch Queue
创建:dispatch_group_t group = dispatch_group_create();
监视处理执行的结束:dipatch_group_notify(group,queue/*可能是main队列*/,方法block);
使用举例:
dispatch_queue_t queue = dispatch_queue_create("com.sxh.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{NSLog(@"blk2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});//也可以使用dispatch_group_wait(group,DISPATCH_TIME_FOREVER)等待全部函数处理结束。若果dispatch_group_wait的返回值不为0,那么就意味着虽然经过了指定时间,但属于Dispatch Group的某一个处理还在执行中。
//这里等待wait的意思是执行dispatch_group_wait函数的当前线程停止。
//dispatch_release(group);
栅栏函数:dispatch_barrier_async (一般用Concurrent Dispatch Queue)
为避免数据竞争并且能高效率的访问,我们一般将读写处理追加到Concurrent Dispatch Queue中,写入处理在任一个读取处理没执行的状态下,追加到Serial Dispatch Queue中。
使用 Concurrent Dispatch Queue和dispatch_barrier_async函数可实现高效率的数据库访问和文件访问。
同步:dispatch_sync
将block追加到指定DispatchQueue中,dispatch_async函数不做任何等待,dispatch_sync函数会一直等待。(等待意味着执行dispatch_sync的线程停止)。
例如:以下情况:在执行主队列时,使用另外的线程GlobalDispatchQueue进行处理,处理结束后立即使用所得到的结果。这时就要使用dispatch_sync
dispatch_queue_t queue = dispatch_queue_create("com.sxh.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{/*处理*/}); //简易版本的wait函数
dispactch_apply():是dispatch_sync和DispatchGroup的关联API,等待全部处理执行结束。推荐在dispatch_async函数中非同步地执行dispatch_apply。
步长迭代非常大的时候才考虑,因为会阻塞当前线程。
dispatch_queue_t queue = dispatch_get_global_queue(默认优先级,0);
dispatch_apply(10,queue,^(size_t index){
NSLog(@"%zu",index);
})
队列挂起:dispatch_suspend(queue);和dispatch_resume(queue);
Dispatch Semaphore(不明白这个计数是怎么工作的)
*让你控制多个消费者对有限数量资源的访问(实例,可用信号量进行测试网络,可取代自旋锁)*
是持有计数的信号,该计数是多线程编程中的技术类型信号(所谓信号类似于过马路时常用的手旗,可以通过时举起手旗,不可通过时放下手旗。Dispatch Semaphore 计数为0时等待,计数为1或大于1时便减去1而不等待)。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,1ull*NSEC_PER_SEC);
long result = dispatch_semaphore_wait(semaphore,time);//等待Dispatch Semaphore的计数值大于或等于1,时,对该计数进行减法,并从dispatch_semaphore_wait中返回。
if(result == 0){
//可安全的进行排他控制的处理。该处理结束时通过dispatch_semaphore_signal将dispatch_semaphore的计数加1。
}else{
//
}
dispatch_queue_t queue = dispatch_queue_create("com.sxh.gcd.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(88);
NSMutableArray *array = [[NSMutableArray alloc]init];
for (int i = 0;i<1000;++i) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//一直等待,直到计数大于等于1
[array addObject:[NSNumber numberWithInteger:i]];
dispatch_semaphore_signal(semaphore);
});
}
dispatch_once:保证在应用程序中只执行一次指定处理的API
即使在多线程环境下执行,也可保证百分之百安全。(单例使用该函数)。
41 多用派发队列,少用同步锁. 总结:
当多个线程执行同一份代码时,可能会出现问题,这时有
(1)@synchronized(self){}内置同步块(atomic就是用该函数实现的)。
(2)或NSLock对象。然而这只是某种程度上的线程安全
(3)更有效率的方法是使用串行队列同步取方法,异步设置方法。执行异步派发时需要拷贝块。再优化就是改用并发队列,同步取方法,使用栅栏块(只是对并发队列有意义)异步设置方法(读取操作可以并行,但是写入操作必须单独执行)dispatch_barrier_async(queue,block)。将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为。而这么做却不会阻塞执行异步派发的线程。使用同步队列及栅栏块,可以令同步行为更高效。
42 多用GCD,少用performSelector系列方法 总结:
performSelector系列方法在内存管理方面容易缺失,它无法确定将要执行的选择子具体是什么,因为ARC编译器也就无法插入适当的内存管理方法。
SEL selector;
if(/*some condition*/){
selector = @selector(newObject);
}else if (/*some other condition*/){
selector = @selector(copy);
}else{
selector = @selector(someProperty);
}
id ret = [object performSelector:selector];
performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数都受到限制。如果想把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里,用GCD相关方法实现。
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
改成 dispatch_time_t time = dispatch_time(DIPATCH_TIME_NOW,(int64_t)(5.0*NSEC_PER_SEC));
dispatch_after(time,dispatch_get_main_queue(),^(void){[self doSomething]});
dispatch_async(dispatch_get_main_queue(),^{[self doSomething]});
43 掌握GCD(派发队列)及操作队列的使用时机 总结:
操作队列在GCD之前就有了,其中某些设计原理因操作队列而流行,GCD就是基于这些原理构建的。实际上从iOS4之后,操作队列在底层是用GCD来实现的。
NSOperation:
NSInvocationOperation和NSBlockOperation,自定义需重写main和cancel函数。start方法默认于当前线程执行。
NSOperationQueue:
分为主队列和其它队列。添加到queue中的任务会自动start。executing\finished\cancel\waitUntilFinished\void (&completionBlock)(void)
使用操作队列的好处有:
(1)取消某个操作
CGD已经加入queue中的block是无法取消的,原生并不支持,也可以,但是需要很复杂的代码,dispatch_suspend只是暂停开启新的未执行的block。NSOperation有取消操已经设定要准备执行的任务,但也不能取消已经开始的任务,但可以通过NSOperation的cancel机制,添加一个外接变量,标记block是否需要取消。在block中及时检测这个外部变量的状态,当发现需要取消时,停止block中的后续操作,释放资源,达到能及时取消block的目的。
(2)指定操作间的依赖关系
addDependency和removeDependency
(3)指定操作的优先级,表示此操作与队列中其他操作之间的优先关系。优先级高的操作先执行。
(4)重用NSOperation对象,自定义。
(5)通过键值观测机制监控NSOperation对象的属性,譬如可以通过isCancelled属性来判断任务是否已取消等。
NSNotificationCenter用的就是操作队列。
- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^)(NSNotifcation*))block;
有人说尽可能选择高层的OC方法,只有确有必要的时候才求助于底层。但想要确定哪种方案最佳,还要测试一下性能。
44 通过Dispatch Group机制,根据系统资源状况来执行任务 总结:
用法一,将要并发执行的多个任务合为一组,于是调用者就可以知道这些任务何时才能全部执行完毕:
dispatch_group_async(group,queue,block);
方法二:dispatch_group_enter(group) 和dispatch_group_leave(group)必须成对出现
dispatch_group_wait(group,timeout)等待group执行完毕,阻塞当前线程。
dispatch_group_notify(group,queue,block),不阻塞当前线程;
一系列任务可以归入一个dispatch group之中。开发者可以在这组任务执行完毕时获得通知。
通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。
单个队列搭配标准的异步派发也可以实现dispatch_group同样的效果。dispatch_apply(iterations,queue,block);用的队列可以是并发,也可以是串行,但是dispatch_apply会持续阻塞,知道所有任务都执行完毕为止。所以想要在后台执行任务,应使用group。
45 使用dispatch_once来执行只需运行一次的线程安全代码 总结:
此操作是完全线程安全的,且更高效。注意,对于只需执行一次的函数来说,每次调用函数时传入的标记必须都完全相同,所以通常将标记变量声明在static或是global的作用域里。
static 类 *instance = nil;
static dispatch_once_t onceToken;
dispatch_once{&onceToken,^{
_instance =[[类 alloc]init];
}
};
return _instance;
速度是使用锁的两倍。
46 不要使用dispatch_get_current_queue 总结:(调试的时候倒是可以用)
iOS6.0后弃用此函数。用该方法检测当前队列是不是某个特定队列,试图以此来避免执行同步派发时可能遭遇死锁问题是错误的。原因是派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。(队列层级体系:排在某条队列中的块,会在其上级队列(父队列)中执行。层级里地位最高的队列总是全局并发队列)。
不要把存取方法做成可重入的,而是应该确保同步操作所用的队列绝不会访问属性。并发队列可以用多个线程并行执行多个块。目标队列?
队列特定数据:可以把任意数据以键值对的形式关联到队列里。如果根据指定的键获取不到关联数据,那么系统就会沿着层级体系向上查找。直至找到数据或到达根队列为止。使用如下:
dispatch_queue_t queueA = dispatch_queue_create("com.sxh.gcd.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.sxh.gcd.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);//设置了队列的层级关系 测试代码
static int kQueueSpecfic;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &kQueueSpecfic, queueSpecificValue, (dispatch_function_t)CFRelease);
//队列定数据,函数是按照指针值来比较键的,而不是内容,其行为更像是关联引用。而和NSDictionary不同(字典是通过键的对象等同性判断)。
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{NSLog(@"no deadlock");};
CFStringRef retrieveValue = dispatch_get_specific(&kQueueSpecfic);
if (retrieveValue) {
block();//说明当前队列是queueA
}
else{
dispatch_sync(queueA, block);//说明当前队列不是queueA
}
});
下面是一个关于在 dispatch_async 上如何以及何时使用不同的队列类型的快速指导:(各种队列皆可以)
- 自定义串行队列:当你想串行执行后台任务并追踪它时就是一个好选择。这消除了资源争用,因为你知道一次只有一个任务在执行。注意若你需要来自某个方法的数据,你必须内联另一个 Block 来找回它或考虑使用 dispatch_sync。
- 主队列(串行):这是在一个并发队列上完成任务后更新 UI 的共同选择。要这样做,你将在一个 Block 内部编写另一个 Block 。以及,如果你在主队列调用 dispatch_async 到主队列,你能确保这个新任务将在当前方法完成后的某个时间执行。
- 并发队列:这是在后台执行非 UI 工作的共同选择。
不知道何时适合使用 dispatch_after ?(相当于延迟的dispatch_async,最好于主队列上使用)
- 自定义串行队列:在一个自定义串行队列上使用 dispatch_after 要小心。你最好坚持使用主队列。
- 主队列(串行):是使用 dispatch_after 的好选择;Xcode 提供了一个不错的自动完成模版。
- 并发队列:在并发队列上使用 dispatch_after 也要小心;你会这样做就比较罕见。还是在主队列做这些操作吧。
下面是你何时会——和不会——使用障碍函数的情况:(最好在自定义并发队列上使用)
- 自定义串行队列:一个很坏的选择;障碍不会有任何帮助,因为不管怎样,一个串行队列一次都只执行一个操作。
- 全局并发队列:要小心;这可能不是最好的主意,因为其它系统可能在使用队列而且你不能垄断它们只为你自己的目的。
- 自定义并发队列:这对于原子或临界区代码来说是极佳的选择。任何你在设置或实例化的需要线程安全的事物都是使用障碍的最佳候选。
下面是一个快速总览,关于在何时以及何处使用 dispatch_sync :(最好在并发队列上使用)
- 自定义串行队列:在这个状况下要非常小心!如果你正运行在一个队列并调用 dispatch_sync 放在同一个队列,那你就百分百地创建了一个死锁。
- 主队列(串行):同上面的理由一样,必须非常小心!这个状况同样有潜在的导致死锁的情况。
- 并发队列:这才是做同步工作的好选择,不论是通过调度障碍,或者需要等待一个任务完成才能执行进一步处理的情况。