一.进程&线程
- 进程:是程序执行过程中
分配和管理资源
的一个基本单位。 - 线程:是程序执行过程中
任务调度和执行
的一个基本单位。 - 一个进程里面有多个线程,
线程是进程的一部分
。
二. 任务
任务:就是要执行什么操作。
1. 同步执行(sync
):
- 在当前的线程上面执行任务。
- 不具备开启新线程的能力。
2. 异步执行(async
):
- 可以开启新的线程执行任务。
- 具备开启新线程的能力。
三. 队列
队列:用于存放任务,遵循FIFO(先进先出)的原则。
1. 串行队列(serial
):
- 队列中每次只执行一个任务,当第一个执行完才能执行第二个。
- 只能开启一个线程。
2. 并发队列(concurrent
):
- 队列中可以同时执行多个任务。
- 可以开启多个线程。
3. 两种特殊队列:
- 全局队列(
dispatch_get_global_queue
):直接作为普通并发队列使用。 - 主队列(
dispatch_get_main_queue
):任务在主线程中执行的串行队列。
四. 使用步骤
注:把任务
放到...(主队列 & 串行队列 & 全局队列
)队列中...(同步 & 异步
)执行。
1. 创建队列
- 串行队列 & 并发队列
// 串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_SERIAL);
// 并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_CONCURRENT);
注:第一个参数表示队列的唯一标识符
,DEBUG
的时候用的,可以为空,推荐使用类似于APP的BundleID
这种逆序域名;第二个参数识别是串行队列
还是并发队列
,串行队列
用DISPATCH_QUEUE_SERIAL
,并发队列用DISPATCH_QUEUE_CONCURRENT
。
- 主队列 & 全局队列
// 主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
注:主队列不用传参数。全局队列第一个参数一般用DISPATCH_QUEUE_PRIORITY_DEFAULT
,表示优先级的。第二个参数暂时没用,用0
即可。
2. 创建任务
// 同步任务创建
dispatch_sync(queue, ^{
});
// 异步任务创建
dispatch_async(queue, ^{
});
注:参数queue
就是队列的类型。
3. 小结
-
并发队列
和全局队列
使用场景是一致的,通常都是使用全局队列
。 - 总共有
3
种队列:全局队列
,主队列
,串行队列
;有2
种任务:同步任务
,异步任务
。 - 共有
6
种使用方式,能否开启新的线程有同步
或者异步
决定,但是否开启新的线程要看当前状况是否需要
开启新的线程来决定。 -
sync
会照成阻塞
现象,sync
任务下的队列里面的任务要必须完成一个才能继续下一个。 -
sync
阻塞的是队列,不是线程。
五. 使用
1. 同步主队列 syncMain
解读:把任务
放到主队列
中同步执行
。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务1");
NSLog(@"任务2");
});
如果在主线程中执行
syncMain
,会出现死锁
现象。
解释:在主线程上面执行同步主队列syncMain
任务,相当于把syncMain
任务加到了主队列Main
中。当要执行任务1
的时候,也要先把任务1
加到Main
中。因为是同步任务sync
,所以要执行任务1
之前,必须先等待syncMain
任务完成。但是要完成syncMain
任务前,又必须执行任务1
,这时候syncMain
任务和任务1
之间就会互相等待,出现死锁
现象,程序崩溃。如果是在子线程中使用
syncMain
,不开启新的线程。先完成任务1
,再完成任务2
。
解释:因为是同步任务sync
,不具备开启新的线程的能力。在子线程中执行syncMain
,相当于吧syncMain
任务加到了子线程的队列(这里用队列A
表示)中。当要执行任务1
和任务2
的时候,把任务1
和任务2
加到主队列Main
中,这个时候,Main
中是没有其他任务的,所以不会出现死锁
现象。Main
是特殊的串行队列,所以先执行完任务1
,再执行任务2
。
2. 异步主队列 asyncMain
解读:把任务
放到主队列
中异步执行
。
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"任务1");
NSLog(@"任务2");
});
不开启新的线程。先完成任务1
,再完成任务2
。
解释:虽然是异步任务async
,具备开启新的线程的能力。但是由于是在主队列Main
中执行任务,Main
中的任务必须在主线程完成,所以不需要开启新的线程。在主线程中执行asyncMain
任务,相当于把asyncMain
任务加到主队列Main
中。当要执行任务1
的时候,也要任务1
加到Main
中。因为是异步任务async
,所以asyncMain
任务可以先等待,先执行完任务1
,再执行任务2
,所以不会出现死锁
现象;在子线程执行asyncMain
任务,跟子线程执行syncMain
任务同等逻辑。
使用场景:做网络请求从后台接口获取到数据之后,需要根据数据更新界面UI,一般都是用asyncMain
,在asyncMain
的block
里面执行刷新界面的操作。
3.同步串行队列 syncSerial
解读:把任务
放到串行队列
中同步执行
。
dispatch_sync(dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_SERIAL), ^{
NSLog(@"任务1");
NSLog(@"任务2");
});
不会开启新的线程。先完成任务1
,再完成任务2
。
解释:因为是同步任务sync
,sync
不具备开启新的线程。执行syncSerial
任务时,syncSerial
任务存放在当前线程的队列(这里用队列B
表示)中,执行任务1
的时候,将任务1
放到当前线程的串行队列Serial
中,任务2
也一样。因为是Serial
,所以要先执行完任务1
,再执行任务2
。
问题:同步串行队列syncSerial
在主线程上执行,为什么不会出现死锁
现象?
回答:在主线程上执行syncSerial
任务,syncSerial
任务存放在主队列Main
当中,而任务1
和任务2
都放在主线程的串行队列Serial
中(不是在主队列Main
中哦~)。此时,主线程上面有两个队列,一个是存放syncSerial
任务的Main
,另一个是存放任务1
和任务2
的Serial
。当执行syncSerial
任务中的任务1
时,会从主队列Main
去到串行队列Serial
,然后在Serial
继续执行任务2
,执行完任务2
,回到主队列Main
中完成syncSerial
任务。
4.异步串行队列 asyncSerial
解读:把任务
放到串行队列
中异步执行
。
dispatch_async(dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_SERIAL), ^{
NSLog(@"任务1");
NSLog(@"任务2");
});
会开启一条新的线程。先完成任务1
,再完成任务2
。
解释:因为是异步任务async
,具备开启新的线程的能力。因为是在串行队列Serial
中,只能同时执行一个任务,所以只需要开启一条新的线程。asyncSerial
任务存放在当前线程的队列中(这里用队列C
表示),而任务1
和任务2
存放在新开启的线程的Serial
。当执行asyncSerial
任务,要开始执行任务1
时,先去到新开启的线程的Serial
中,执行完任务1
,再执行任务2
,然后回到队列C
中完成asyncSerial
任务。
5. 同步全局队列 syncGlobal
解读:把任务
放到全局队列
中同步执行
。
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"任务1");
NSLog(@"任务2");
});
不会开启新的线程。先完成任务1
,再完成任务2
。
解释:因为是同步任务sync
,不具备开启新的线程能力。虽然是在全局队列Global
中,可以多个任务同时进行,但是只有一条线程,所以还是要先完成任务1
再执行任务2
。syncGlobal
任务存放在当前线程的队列(这里用队列D
表示)中,执行任务1
的时候,将任务1
放到当前线程的全局队列Global
中,任务2
也一样。虽然是Global
,但是只有一条线程,所以要先执行完任务1
,再执行任务2
。
使用场景:上传多张图片到后台,后台要求一张一张的上传。
6. 异步全局队列 asyncGlobal
解读:把任务
放到全局队列
中异步执行
。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"任务1");
NSLog(@"任务2");
});
会开启多条新的线程。任务1
和任务2
同时执行。
解释:因为是异步任务async
,async
会具备开启新的线程能力。因为是全局队列Global
,所以Global
里面的任务可以同时执行。asyncGlobal
任务存放在当前线程的队列(这里用队列E
表示)中,而任务1
和任务2
存放在各自开启的线程队列中。当执行asyncGlobal
任务,因为是Global
,所以任务1
和任务2
可以同时执行。
使用场景:上传多张图片到后台,可以多张同时上传。
六.GCD线程之间的通信
异步开启子线程执行耗时任务,耗时任务完成,利用主队列回到主线程更新UI。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 10; i ++) {
NSLog(@"---%d",i);
// 模拟耗时操作
[NSThread sleepForTimeInterval:1];
}
});
dispatch_async(dispatch_get_main_queue(), ^{
// 耗时操作完成
NSLog(@"任务完成,回到主线程更新UI。");
});
});
七.阻塞方法dispatch_barrier
作用:在有多个任务并且使用栅栏方法dispatch_barrier
的队列(注意:不能使用全局队列
),必须先等待dispatch_barrier
前面的任务执行完毕,才能执行dispatch_barrier
里面的任务。等待dispatch_barrier
里面的任务执行完毕,才能继续执行dispatch_barrier
之后的任务。
例子:有三种图片,分别压缩之后,一起上传后后台。
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"压缩图片1");
});
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"压缩图片2");
});
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:4];
NSLog(@"压缩图片3");
});
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"将 压缩图片1 压缩图片2 压缩图片3 上传到后台");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"其他操作");
});
结果:这样子既能让3张图片同时压缩,又能确保3张图片都压缩完之后,才将3张图片上传到后台。
问题1:为什么不能使用 全局队列 ?
解释:苹果官方给的说明是如果使用全局队列,那么dispatch_barrier_async
方法将退化成dispatch_async
方法。个人觉得,不知道对不对,全局队列没有名字,自定义的并发队列是有名字的,系统需要重新控制队列里面任务的执行操作,必须具体到哪个队列中去重新控制。
问题2:dispatch_barrier_asyn
和dispatch_barrier_syn
的区别?
解释:上例中,将dispatch_barrier_asyn
替换成dispatch_barrier_syn
效果是一样的。它们的区别在于,
-
dispatch_barrier_asyn
将自己的任务加入到队列中之后,不用等自己的任务执行完毕,它就将它后面的任务也加入到队列中。然后等待自己的任务执行完毕,才执行后面的任务。 -
dispatch_barrier_syn
将自己的任务加入到队列中之后,需要等待自己的任务执行完毕,才能加入后台的任务,并执行后面的任务。
八.延时方法 dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
方法中需要传入一个延时的时间(秒),延时操作里面的任务放到主队列
执行。
九.只执行一次(单例) dispatch_once
+ (instancetype)shareInstance{
static Singleton *single;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
single = [[Singleton alloc] init];
});
return single;
}
在程序运行过程中,dispatch_once
方法中的代码只会被执行1次,即不影响性能,又能保证线程安全。
-
原理:
dispatch_once
方法是根据dispatch_once_t
修饰的变量onceToken
的值来决定接下来的操作的。
(1)当onceToken = 0
时,说明程序第一次执行dispatch_once
方法,直接执行dispatch_once
的block
中的代码。
(2)当onceToken = -1
时,说明程序已经执行完过dispatch_once
方法,那么跳过dispatch_once
的block
的代码,执行block
之后的代码。
(3)当onceToken != 0
且onceToken != -1
时,说明现在有线程(这里用线程A
表示)在执行dispatch_once
方法,但是还没执行完毕。这个时候,当前这条线程处于阻塞状态
,等待线程A
执行完毕。当线程A
执行完dispatch_once
方法时,onceToken
的值会变成-1
,这时候当前这条线程继续执行。
(4)单例
可以看成是一种特殊的实例,是一个全局的对象,有且只有一个的对象。
十.快速迭代 dispatch_apply
NSLog(@"---");
dispatch_apply(10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
// 模拟耗时操作
[NSThread sleepForTimeInterval:(11 - index)];
NSLog(@"%zu",index);
});
NSLog(@"+++");
---
3
2
1
0
6
5
4
7
9
8
+++
dispatch_apply
是一个快速迭代的方法,类似于for循环
。
- 如果方法中传入的是一个
串行队列
,那么dispatch_apply
里面的耗时操作就需要按顺序同步执行(相当于异步串行队列
,必须执行完成一个任务之后,才能执行下一个任务。不过一般不会这么做,这样操作就失去了快速迭代
的意义)。 - 如果方法中传入的是一个
全局队列
,那么里面多个耗时操作就可以同时进行(相当于异步并发队列
,可以多个任务同时执行)。 - 无论传入的是
串行队列
还是全局队列
,dispatch_apply
方法都会阻塞当前线程
等待所有任务执行完毕,才能执行dispatch_apply
方法后面的代码(相当于同步任务
)。
十一. 队列组 dispatch_group
需求:在填写个人资料页面,我们需要把个人的信息(名字,手机号等)上传到后台,也需要把照片(身份证正反面拍照等)也上传到后台,需要做两个网络请求。当两个网络请求都成功回调之后,返回上一个页面。
1. 第一种方法:使用dispatch_group_notify
监听。
NSLog(@"---");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:3];
NSLog(@"上传个人信息");
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 3; ++i) {
[NSThread sleepForTimeInterval:1];
NSLog(@"上传图片资料");
}
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"返回上个界面");
});
NSLog(@"===");
---
===
上传图片资料
上传图片资料
上传个人信息
上传图片资料
上传个人信息
返回上个界面
-
dispatch_group_create()
创建一个队列组。 -
dispatch_group_async
将任务放到队列里面,然后再讲队列放到队列组里面。 -
dispatch_group_notify
监听队列组中其他队列的任务完成状态,当所有的任务都执行完成之后,将自身block
里面的任务也方法队列组中,执行任务。 -
dispatch_group_notify
不会阻塞当前线程。
2. 第二种方法:使用dispatch_group_wait
阻塞当前线程。
NSLog(@"---");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:3];
NSLog(@"上传个人信息");
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 3; ++i) {
[NSThread sleepForTimeInterval:1];
NSLog(@"上传图片资料");
}
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"返回上个界面");
NSLog(@"===");
---
上传图片资料
上传图片资料
上传个人信息
上传图片资料
上传个人信息
返回上个界面
===
- 当所有任务都完成之后,才执行
dispatch_group_wait
后面的任务。 -
dispatch_group_wait
会阻塞当前的线程。
3. 第三种方法:使用dispatch_group_enter
和dispatch_group_leave
组合代替dispatch_group_async
。
NSLog(@"---");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:3];
NSLog(@"上传个人信息");
}
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
for (int i = 0; i < 3; ++i) {
[NSThread sleepForTimeInterval:1];
NSLog(@"上传图片资料");
}
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"返回上个界面");
});
NSLog(@"===");
---
===
上传图片资料
上传图片资料
上传个人信息
上传图片资料
上传个人信息
返回上个界面
-
dispatch_group_enter
表示把一个任务放到队列组group
,并开始执行。 -
dispatch_group_leave
表示任务执行完毕。 -
dispatch_group_enter
+dispatch_group_leave
=dispatch_group_async
- 可以用
dispatch_group_notify
,同样也可以用dispatch_group_wait
(结果跟第2种
方法一样)。区别是后者会照成当前线程阻塞
,前者不会。 - 当
group
中的所有任务都执行完毕时,才会执行dispatch_group_wait
后面的任务,或者执行追加到dispatch_group_notify
中的任务。
十二. 信号量 dispatch_semaphore
三个重要方法
-
dispatch_semaphore_create
:创建并初始化一个信号总量,一般为0
或者1
。 -
dispatch_semaphore_signal
:发送一个信号,即让信号总量+1
。 -
dispatch_semaphore_wait
:如果当前信号总量为0
,那么阻塞当前线程
,否则。信号总量-1
,正常执行。
1. 异步线程变成同步。
需求:有时候需要实时拿到异步里面耗时操作的结果,才能正确的执行之后的代码。
__block NSInteger i = 1;
// 创建一个信号总量为`0`的信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2];
i ++;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%ld",(long)i);
打印结果为:2
解释:当第一次执行dispatch_semaphore_wait
时,信号总量为0
,当前线程阻塞
。当执行完异步block
里面的耗时操作之后,执行了dispatch_semaphore_signal
,信号总量+1
。从block
里面出来第二次执行dispatch_semaphore_wait
时,信号总量为1
,正常执行。这样就能等到异步执行完之后,再执行接下来的代码(类似于同步执行)。
2. 线程安全(线程锁)
需求:有时候,我们会在多个地方同时对同一个接口进行调用,那如果每次调用过程会对下一次调用的结果有影响(有修改或者更变等操作),那么我们就必须保证该接口同一时间只能被一个地方调用,这就是线程安全
。
- (void)viewDidLoad {
[super viewDidLoad];
self.semaphore = dispatch_semaphore_create(1);
}
- (void)tiaoyongjiekou{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟耗时操作
[NSThread sleepForTimeInterval:1.0];
dispatch_semaphore_signal(weakSelf.semaphore);
});
}
解释:在外部创建一个信号总量为1
的信号量,当第一次调用tiaoyongjiekou
方法,执行到dispatch_semaphore_wait
时,因为当前信号总量为1
,那么正常执行并且信号总量-1
(此时信号总量为0
)。如果第一次调用还没执行完成,第二次就开始调用,当执行到dispatch_semaphore_wait
时,信号总量为0
,线程阻塞,只能原地等待。等第一次调用结束,执行完耗时操作之后,执行了dispatch_semaphore_signal
,信号总量+1
(此时信号总量为1
),那么第二次调用才能继续执行。这样就能确保同一时间只被调用一次,确保线安全。