iOS开发进阶:多线程与GCD
一、多线程
1.1进程与线程
进程:进程是指在系统中正在运行的一个应用程序;每个进程之间是独立的,每个进程均运行在其专用的受保护的内存空间内。
线程:线程是进程的基本执行单元,一个进程的所有任务都在线程中执行;进程要想执行任务,必须要有线程,进程至少要有一条线程;程序启动回默认开启一条线程,即主线程。
多线程原理:同一时间单核CPU只能处理一个线程,即只有一个线程在执行。多线程同时执行是CPU快速的在多个线程之间切换,CPU调度线程的时间足够快,就造成了多线程同时执行的效果。如果线程非常多,CPU会在N个线程之间切换,消耗大量的CPU资源,每个线程被调度的次数会降低,线程的执行效率也会降低。
多线程技术方案:
方案 | 说明 | 语言 | 生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 一套通用的多线程API;适用于Unix/Linux/Windows等平台;跨平台,可移植;适用难度大 | c | 开发管理 | 很低 |
NSThread | 使用更加面向对象;简单易用,可直接操作线程对象 | OC | 开发管理 | 低 |
GCD | 旨在替代NSThread技术,充分利用设备的多核 | c | 自动管理 | 高 |
NSOperation | 基于GCD(底层是GCD);比GCD多了一些更简单实用的功能;实用更加面向对象 | OC | 自动管理 | 高 |
1.2 任务
任务是指执行的操作,简单说就是在线程中执行的那段代码。在GCD中是放在block中的。
任务的执行有两种方式:同步执行和异步执行。两者的区别主要在于是否等待队列中的任务执行完毕,以及是否具备开启新线程的能力。
- 同步执行(sync):同步添加任务到指定的队列中,在添加的任务执行结束前会一直等待,直到队列里的任务完成后再继续执行;只能在当前线程执行任务,不具备开启新线程的能力。加入方式
dispatch_sync
。 - 异步执行(async):异步添加任务到指定队列,不会做任何等待,可以继续执行任务;可以在新的线程执行任务,具备开启新线程的能力,但是并不一定开启新线程,这跟任务所在的队列有关。加入方式
dispatch_async
。
任务执行速度的影响因素:1.CPU。2.线程状态。3.任务的复杂度。4.任务的优先级。其中任务的优先级包括用户指定的qualityService
(userInteractive
, userInitiated
,utility
,background
,default
);等待的频繁程度(不执行)。
二、GCD
队列(Dispatch Queue)指的是执行任务的等待队列,即用来存放任务的队列。队列采用FIFO(先进先出)的原则,新任务总是被插入到队列的末尾,读取任务的时候总是从队列的头部开始读取,每读取一个任务,则从队列中释放一个任务。
GCD中有2种队列:串行队列和并发队列,两者均遵循FIFO的原则,不同点在于执行的顺序以及开启线程的数量。
- 串行队列(Serial Dispatch Queue):只开启一个线程,一个任务执行完毕才会执行下一个任务。每次只有一个任务被执行。
- 并发队列(Concurrent Dispatch Queue):可以开启多个线程,并且同时执行任务。可以让多个任务同时执行。
队列的创建:可以使用dispatch_queue_create
创建,该方法需要传入2个参数:第一个参数表示队列的标识,可为空;第二个参数用来识别是串行队列还是并发队列,DISPATCH_QUEUE_SERIAL
(==NULL)标识串行队列,DISPATCH_QUEUE_CONCURRENT
//串行队列
dispatch_queue_t s = dispatch_queue_create("com.appex.queue.serial", DISPATCH_QUEUE_SERIAL);
//并发队列
dispatch_queue_t c = dispatch_queue_create("com.appex.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
主队列:主队列(Main Dispatch Queue)是一种特殊的串行队列,说它特殊是因为默认情况下代码就在主队列中,主队列的代码又都会放在主线程中执行。获取方式:
//获取主队列
dispatch_group_t main = dispatch_get_main_queue();
全局并发队列:全局并发队列是(Global Dispatch Queue)是系统提供的并发队列,获取方法:
//获取全局并发队列
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//第一个参数表示优先级的高低,一般传`DISPATCH_QUEUE_PRIORITY_DEFAULT`
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND
//第二个参数暂时没用,传0即可。
如果当前在主线程,按照队列的串行和并发,任务的同步和异步特性组合,我们归纳如下:
区别 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步 | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 死锁 |
异步 | 有开启新线程,并发执行任务 | 有开启新线程(1条),串行执行任务 | 没有开启新线程,串行执行任务 |
/*死锁案例-1*/
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
/*async没有开启新线程,以下代码在主线程中运行*/
NSLog(@"开始:%@", [NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"sync:%@", [NSThread currentThread]);
});
NSLog(@"结束:%@", [NSThread currentThread]);
});
在主线程中,向主队列添加同步任务会死锁。这是因为添加的任务和主队列自身的任务相互等待,阻塞了主队列,最终造成主队列所载的线程(主线程)死锁。如果在其他线程向主队列添加同步任务,则不会死锁。
/*死锁案例-2*/
dispatch_queue_t queue = dispatch_queue_create("com.app.serial", 0);
dispatch_async(queue, ^{
/*async开启1条新线程,以下代码在子线程中运行*/
NSLog(@"开始:%@", [NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"sync:%@", [NSThread currentThread]);
});
NSLog(@"结束:%@", [NSThread currentThread]);
});
在子线程中,向串行队列添加同步任务会死锁。这是因为添加的任务和串行队列自身的任务相互等待,阻塞了串行队列,最终造成串行队列所在的线程(子线程)死锁。如果在其他线程向该队列添加同步任务,则不会死锁。
以上案例可以概括为在一个串行队列所在的线程,向该队列添加同步任务会造成串行队列追加的任务和原有的任务相互等待而阻塞当前线程。
三、GCD其他常用函数
3.1 dispatch_after:表示在某个队列中异步延迟执行任务
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t work){
_dispatch_after(when, queue, NULL, work, true);
}
第一个参数when表示开始的时间,通常在现在的时间时间的基础上加时间,如dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2)
表示2秒之后;第二个参数queue
传入队列,第三个参数work
传入任务的block
代码。
常规用法如下:
NSLog(@"开始:%@", [NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2), dispatch_get_main_queue(), ^{
NSLog(@"after:%@", [NSThread currentThread]);
});
3.2 dispatch_once:代码只执行一次
void dispatch_once(dispatch_once_t *val, dispatch_block_t block){
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
第一个参数val
传入一个dispatch_once_t
类型的指针地址,第二个参数block
传入只执行一次的block
任务。
常规用法:
@interface NXDownloader : NSObject
+ (NXDownloader *)downloader;
@end
@implementation NXDownloader
+ (NXDownloader *)downloader{
static dispatch_once_t t;
static NXDownloader *sharedInstance;
dispatch_once(&t, ^{
sharedInstance = [[NXDownloader alloc] init];
});
return sharedInstance;
}
@end
这样我们通过NXDownloader *downloader = [NXDownloader downloader];
获取到的实例都是同一个,而且是线程安全的。
3.3dispatch_barrier_async/dispatch_barrier_sync:栅栏函数
栅栏函数的作用就是隔离栅栏函数之前与之后的代码执行,只有前面的代码执行完毕才会执行后面的代码,函数如下:
void dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work){
...
}
void dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work){
...
}
栅栏函数的第一个参数dq
接收一个队列,按照代码注释的说明,这里需要传入一个并发队列才会真正的发挥栅栏函数的功能,如果传入的dq是个串行队列,则函数的表现与dispatch_barrier_async
和dispatch_barrier_sync
表现一样。
那么这里的async
和sync
的作用有什么不同呢?看如下代码:
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"1:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"2:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"3:%@", [NSThread currentThread]);
});
NSLog(@"0-0:%@",[NSThread currentThread]);
//dispatch_barrier_async或dispatch_barrier_sync
dispatch_barrier_async(queue, ^{
sleep(2);
NSLog(@"barrier:%@", [NSThread currentThread]);
});
NSLog(@"0-1:%@",[NSThread currentThread]);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"4:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"5:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"6:%@", [NSThread currentThread]);
});
多次打印,结果如下:
async.png sync.png
执行结果显示:{1,2,3}
执行的顺序是不确定,{4,5,6}
执行顺序也是不确定的,可以确定的是{1,2,3}
执行完毕后执行barrier
,再执行{4,5,6}
。在async的情况下0-0
,0-1
最先执行。而sync
的情况下0-0在barrier
之前执行,0-1
在barrier
之后执行。也就是sync
会如同dispatch_sync
一样执行完毕后再执行后面的代码。并且在sync
的情况下barrier
任务会在原有的线程(这里是主线程)中执行。
还需要注意一点,栅栏函数的只在自定义的并发队列才会生效,这一点也好理解,因为并发队列,系统也会向里边添加任务,我们设置一个栅栏那么后续加入的系统任务岂不是要等待栅栏执行完毕?这显然不合理。
3.4dispatch_group,队列组
队列组简言之就是一组任务执行完毕后会有一个单独的回调。
//队列组的创建
dispatch_group_t group = dispatch_group_create();
//添加任务
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
NSLog(@"执行");
});
//任务执行完毕的回掉
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"结束:%@");
});
其中dispatch_group_async
会把任务放入队列,再把队列放入队列组。也可以用dispatch_group_enter
和dispatch_group_leave
成对使用。 dispatch_group_notify
会在任务执行完毕后回调,你可以指定一个队列继续做其他事情。还有一个不太常用的dispatch_group_wait
函数,这个函数第一个参数传入一个group,第二个参数传入一个time
,这个时间指定的是dispatch_group_wait
之后的代码等待的最大时间,假定这里设定的时间是3秒,如果前面的任务2秒执行完毕,那么wait后面的代码会在2秒后执行。如果前面的任务4秒执行完毕,那么wait后面的代码会在第3秒的时候开始执行。
dispatch_group_notify案例
//dispatch_group_notify案例:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
for (int i= 0; i < 10; i++){
dispatch_group_async(group, queue, ^{
NSLog(@"%d:%@", i, [NSThread currentThread]);
});
/*以上三行代码等价于
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"%d:%@", i, [NSThread currentThread]);
dispatch_group_leave(group);
});
*/
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"结束:%@", [NSThread currentThread]);
});
dispatch_group_wait案例
//dispatch_group_wait案例
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
for (int i= 0; i < 10; i++){
dispatch_group_async(group, queue, ^{
sleep(2);
NSLog(@"%d:%@", i, [NSThread currentThread]);
});
}
//dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC*2)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"结束:%@", [NSThread currentThread]);
这个场景在实际开发中使用较多,比如现在有一组网络图片需要分享到某个平台,我们需要等待多张网络图片全部下载完毕后才能开始分享。