pthread、NSThread、GCD、NSOperation。
iOS的app基本上都是单进程的,iOS的WKWebView却是罕见的进程组件,所以WKWebView和其它组件完全不同,它存在cookie同步不及时问题。所以分析iOS的app只需要按照一个单进程分析就可以了,多线程才是我们研究的重点。
多线程的原理:
同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)
多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)
如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
思考:如果线程非常非常多,会发生什么情况?
CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源
每条线程被调度执行的频次会降低(线程的执行效率降低) 。
注意:程序创建线程成功,这行并不是说线程就起动成功了,而是向系统申请创建线程成功了,只有等线程函数(大括号里面的代码)中代码被执行了才能说明线程创建并启动成功,你可以在线程函数中打印日志,可以观察到正常情况下,从线程创建完毕,到线程启动大约有几十毫秒的时间,但是你快速循环创建线程,那么这个时间达到100多毫秒也是会发生的,甚至发生有的线程函数永远不被执行,也就是线程没有实际启动成功。当然并不说只循环创建线程才出现线程起不来的情况,当应用是非后台运行的应用,当应用切换到后台,所有的线程被挂起,当应用切到后台时,正好是线程创建成功而没有正式启动成功,那么就存在线程永远起不来的情况。
多线程的类别:
第一种:pthread
.特点:
1)一套通用的多线程API
2)适用于Unix\Linux\Windows等系统
3)跨平台\可移植
4)使用难度大
b.使用语言:c语言
c.使用频率:几乎不用
d.线程生命周期:由程序员进行管理
第二种:NSThread
a.特点:
1)使用更加面向对象
2)简单易用,可直接操作线程对象
b.使用语言:OC语言
c.使用频率:偶尔使用
d.线程生命周期:由程序员进行管理
第三种:GCD
a.特点:
1)旨在替代NSThread等线程技术
2)充分利用设备的多核(自动)
b.使用语言:C语言
c.使用频率:经常使用
d.线程生命周期:自动管理
第四种:NSOperation
a.特点:
1)基于GCD(底层是GCD)
2)比GCD多了一些更简单实用的功能
3)使用更加面向对象
b.使用语言:OC语言
c.使用频率:经常使用
d.线程生命周期:自动管理
多线程的优点:
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)
多线程的缺点:
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调度线程上的开销就越大
程序设计更加复杂:比如线程之间的通信、多线程的数据共享
你更倾向于哪一种?
倾向于GCD:
GCD 技术是一个轻量的,底层实现隐藏的神奇技术,我们能够通过GCD和block轻松实现多线程编程,有时候,GCD相比其他系统提供的多线程方法更加有效,当然,有时候GCD不是最佳选择,另一个多线程编程的技术 NSOprationQueue 让我们能够将后台线程以队列方式依序执行,并提供更多操作的入口,这和 GCD 的实现有些类似。
这种类似不是一个巧合,在早期,MacOX 与 iOS 的程序都普遍采用Operation Queue来进行编写后台线程代码,而之后出现的GCD技术大体是依照前者的原则来实现的,而随着GCD的普及,在iOS 4 与 MacOS X 10.6以后,Operation Queue的底层实现都是用GCD来实现的。
那这两者直接有什么区别呢?
1. GCD是底层的C语言构成的API,而NSOperationQueue及相关对象是Objc的对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构;而Operation作为一个对象,为我们提供了更多的选择;
2. 在NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码);
3. NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;
4. 我们能将KVO应用在NSOperation中,可以监听一个Operation是否完成或取消,这样子能比GCD更加有效地掌控我们执行的后台任务;
5. 在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;
6. 我们能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。
总的来说,Operation queue 提供了更多你在编写多线程程序时需要的功能,并隐藏了许多线程调度,线程取消与线程优先级的复杂代码,为我们提供简单的API入口。从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。但是我认为当我们的需求能够以更简单的底层代码完成的时候,简洁的GCD或许是个更好的选择,而Operation queue 为我们提供能更多的选择。
倾向于:NSOperation
NSOperation相对于GCD:
1,NSOperation拥有更多的函数可用,具体查看api。NSOperationQueue 是在GCD基础上实现的,只不过是GCD更高一层的抽象。
2,在NSOperationQueue中,可以建立各个NSOperation之间的依赖关系。
3,NSOperationQueue支持KVO。可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)
4,GCD 只支持FIFO 的队列,而NSOperationQueue可以调整队列的执行顺序(通过调整权重)。NSOperationQueue可以方便的管理并发、NSOperation之间的优先级。
使用NSOperation的情况:各个操作之间有依赖关系、操作需要取消暂停、并发管理、控制操作之间优先级,限制同时能执行的线程数量.让线程在某时刻停止/继续等。
使用GCD的情况:一般的需求很简单的多线程操作,用GCD都可以了,简单高效。
从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。
当需求简单,简洁的GCD或许是个更好的选择,而Operation queue 为我们提供能更多的选择。
若你只是建立一个线程处理一些简单的事情,不想关注具体的底层的细节,GCD足够满足满足你的需要了。但是你要实现复杂的功能,需要底层技术支持,那么使用pthread也无可厚非。使用那种线程是按需选择,没有绝对高贵与低贱。我更喜欢下里巴人的GCD。
GCD使用的就一个例子。
-(void)customTimerStart { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[NSThread currentThread] setName:@"custom Timer"]; }); //注意,程序运行到这行并不是说线程起动成功了,而是向系统申请创建线程成功了,只有等线程函数(大括号里面的代码)中代码被执行了才能说明线程创建并启动成功,你可以在线程函数中打印日志,可以观察到正常情况下,从线程创建完毕,到线程启动大约有几十毫秒的时间,但是你快速循环创建线程,那么这个时间达到100多毫秒也是会发生的。 });
既然GCD那么好,我们就重点介绍下它吧!
GCD队列的概念
在多线程开发当中,程序员只要将想做的事情定义好,并追加到DispatchQueue(派发队列)当中就好了。
派发队列分为两种,一种是串行队列(SerialDispatchQueue),一种是并行队列 (ConcurrentDispatchQueue)。
一个任务就是一个block,比如,将任务添加到队列中的代码是:
1 dispatch_async(queue, block);
当给queue添加多个任务时,如果queue是串行队列,则它们按顺序一个个执行,同时处理的任务只有一个。
当queue是并行队列时,不论第一个任务是否结束,都会立刻开始执行后面的任务,也就是可以同时执行多个任务。
但是并行执行的任务数量取决于XNU内核,是不可控的。比如,如果同时执行10个任务,那么10个任务并不是开启10个线程,线程会根据任务执行情况复用,由系统控制。
获取队列
系统提供了两个队列,一个是MainDispatchQueue,一个是GlobalDispatchQueue。
前者会将任务插入主线程的RunLoop当中去执行,所以显然是个串行队列,我们可以使用它来更新UI。
后者则是一个全局的并行队列,有高、默认、低和后台4个优先级。
它们的获取方式如下:
dispatch_queue_t queue = dispatch_get_main_queue(); dispatch queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRORITY_DEFAULT, 0)
执行异步任务
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ //... });
这个代码片段直接在子线程里执行了一个任务块。使用GCD方式任务是立即开始执行的
它不像操作队列那样可以手动启动,同样,缺点也是它的不可控性。
令任务只执行一次
+ (id)shareInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _shareInstance = [[self alloc] init]; }); }
这种只执行一次且线程安全的方式经常出现在单例构造器当中。
任务组
有时候,我们希望多个任务同时(在多个线程里)执行,再他们都完成之后,再执行其他的任务,
于是可以建立一个分组,让多个任务形成一个组,下面的代码在组中多个任务都执行完毕之后再执行后续的任务:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ NSLog(@"1"); }); dispatch_group_async(group, queue, ^{ NSLog(@"2"); }); dispatch_group_async(group, queue, ^{ NSLog(@"3"); }); dispatch_group_async(group, queue, ^{ NSLog(@"4"); }); dispatch_group_async(group, queue, ^{ NSLog(@"5"); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"done"); });延迟执行任务 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //... });
这段代码将会在10秒后将任务插入RunLoop当中。
dispatch_asycn和dispatch_sync
先前已经有过一个使用dispatch_async执行异步任务的一个例子,下面来看一段代码:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ NSLog(@"1"); }); NSLog(@"2");
这段代码首先获取了全局队列,也就是说,dispatch_async当中的任务被丢到了另一个线程里去执行,async在这里的含义是,当当前线程给子线程分配了block当中的任务之后,当前线程会立即执行,并不会发生阻塞,也就是异步的。那么,输出结果不是12就是21,因为我们没法把控两个线程RunLoop里到底是怎么执行的。
类似的,还有一个“同步”方法dispatch_sync,代码如下:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_sync(queue, ^{ NSLog(@"1"); }); NSLog(@"2");
这就意味着,当主线程将任务分给子线程后,主线程会等待子线程执行完毕,再继续执行自身的内容,那么结果显然就是12了。
需要注意的一点是,这里用的是全局队列,那如果把dispatch_sync的队列换成主线程队列会怎么样呢:
dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{ NSLog(@"1"); });
这段代码会发生死锁,因为:
1.主线程通过dispatch_sync把block交给主队列后,会等待block里的任务结束再往下走自身的任务,
2.而队列是先进先出的,block里的任务也在等待主队列当中排在它之前的任务都执行完了再走自己。
这种循环等待就形成了死锁。所以在主线程当中使用dispatch_sync将任务加到主队列是不可取的。
创建队列
我们可以使用系统提供的函数获取主串行队列和全局并行队列,当然也可以自己手动创建串行和并行队列,代码为:
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_SERIAL); dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_CONCURRENT);
在MRC下,手动创建的队列是需要释放的
dispatch_release(myConcurrentDispatchQueue); 手动创建的队列和默认优先级全局队列优先级等同,如果需要修改队列的优先级,需要: dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_CONCURRENT); dispatch_queue_t targetQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_set_target_queue(myConcurrentDispatchQueue, targetQueue);
上面的代码修改队列的优先级为后台级别,即与默认的后台优先级的全局队列等同。
串行、并行队列与读写安全性
在向串行队列(SerialDispatchQueue)当中加入多个block任务后,一次只能同时执行一个block,如果生成了n个串行队列,并且向每个队列当中都添加了任务,那么系统就会启动n个线程来同时执行这些任务。
对于串行队列,正确的使用时机,是在需要解决数据/文件竞争问题时使用它。比如,我们可以令多个任务同时访问一块数据,这样会出现冲突,也可以把每个操作都加入到一个串行队列当中,因为串行队列一次只能执行一个线程的任务,所以不会出现冲突。
但是考虑到串行队列会因为上下文切换而拖慢系统性能,所以我们还是很期望采用并行队列的,来看下面的示例代码:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ //数据读取 }); dispatch_async(queue, ^{ //数据读取2 }); dispatch_async(queue, ^{ //数据写入 }); dispatch_async(queue, ^{ //数据读取3 }); dispatch_async(queue, ^{ //数据读取4 });
显然,这5个操作的执行顺序是我们无法预期的,我们希望在读取1和读取2执行结束后,再执行写入,写入完成后再执行读取3和读取4。
为了实现这个效果,这里可以使用GCD的另一个API:
dispatch_barrier_async(queue, ^{ //数据写入 });
这样就保证的写入操作的并发安全性。
对于没有数据竞争的并行操作,则可以使用并行队列(CONCURRENT)来实现。
JOIN行为
CGD利用dispatch_group_wait来实现多个操作的join行为,代码如下:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ sleep(0.5); NSLog(@"1"); }); dispatch_group_async(group, queue, ^{ sleep(1.5); NSLog(@"2"); }); dispatch_group_async(group, queue, ^{ sleep(2.5); NSLog(@"3"); }); NSLog(@"aaaaa"); dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2ull * NSEC_PER_SEC); if (dispatch_group_wait(group, time) == 0) { NSLog(@"已经全部执行完毕"); } else { NSLog(@"没有执行完毕"); } NSLog(@"bbbbb");
这里起了3个异步线程放在一个组里,之后通过dispatch_time_t创建了一个超时时间(2秒),程序之后行,立即输出了aaaaa,这是主线程输出的,当遇到dispatch_group_wait时,主线程会被挂起,等待2秒,在等待的过程当中,子线程分别输出了1和2,2秒时间达到后,主线程发现组里的任务并没有全部结束,然后输出了bbbbb。
在这里,如果超时时间设置得比较长(比如5秒),那么会在2.5秒时第三个任务结束后,立即输出bbbbb,也就是说,当组中的任务全部执行完毕时,主线程就不再被阻塞了。
如果希望永久等待下去,时间可以设置为DISPATCH_TIME_FOREVER。
并行循环
类似于C#的PLINQ,OC也可以让循环并行执行,在GCD当中有一个dispatch_apply函数:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_apply(20, queue, ^(size_t i) { NSLog(@"%lu", i); });
这段代码让i并行循环了20次,如果内部处理的是一个数组,就可以实现对数组的并行循环了,它的内部是dispatch_sync的同步操作,所以在执行这个循环的过程当中,当前线程会被阻塞。
暂停和恢复
使用dispatch_suspend(queue)可以暂停队列中任务的执行,使用dispatch_result(queue)可以继续执行被暂停的队列。
恶补一下GCD线程的知识是否感觉它也不简单吧!若你想玩玩,它也有足够的操作和函数够你玩刷的了。