文章目录
GCD简介
GCD是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现
我们为什么要使用GCD呢
- GCD可用于多核的并行运算
- GCD会自动利用更多的CPU内核
- GCD会自动管理线程的生命周期
- 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理的代码
GCD任务和队列
任务
任务就是执行操作,也就是我们在线程中执行的那段代码。在GCD中是放在Block中的。
具体对任务的执行有两种方式:同步执行和异步执行,主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力
- 同步执行(sync)
-
- 同步添加任务到指定的队列中,在添加的任务执行结束前,会一直等待,直到队列里面的任务完成之后再继续执行
-
- 只能在当前线程中执行任务,不具备开启新线程的能力
- 异步执行(async)
-
- 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务
-
- 可以在新的线程中执行任务,具备开启新线程的能力
一个简单的例子:我们需要打电话给A和B
同步执行:打电话给A的时候,不能同时打给B。只有等到A打完了,才能打给B。(等待任务结束)而且只能用当前电话(不具备开启新线程的能力)
异步执行:打电话给A的时候,不用等着和A通话结束(不用等待任务执行结束),还能同时给B打电话。而且除了当前电话,我们还可以使用其他一个或多个电话(具备开启其他线程的能力)
注意是异步执行虽然具有开启新线程的能力,但是并不一定开启新线程。
队列
指的是执行任务的等待队列,即用来存放新任务的队列。队列是一种特殊的线性表,采用FIFO的原则,即新的任务总是被插入到队列的队尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
队列也是主要分为两种串行队列和并发队列
两者都符合FIFO的原则。两者主要的区别是:执行顺序不同 ,以及开启线程数不同
- 串行队列
-
- 每次只有一个任务被执行。让任务一个接一个的执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
- 并发线程
-
- 可以让多个任务并发(同时执行)。(可以开启多个线程,并且同时执行任务)
-
- 并发队列的并发功能只有在异步方法下才有效
GCD使用步骤
- 创建一个队列(串行队列或并发队列)
- 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)
队列的创建方法 / 获取方法
可以使用dispatch_queue_create
方法来创建队列。该方法需要我们传入两个参数
- 第一个参数用于表示队列的唯一标识符,用于DEBUG,可以为空。但是还是推荐使用应用程序ID这种逆序全程域名。
- 第二个参数用来识别是串行队列还是并发队列。
-
DISPATCH_QUEUE_SERIAL
或指定为NULL,表示串行队列
-
DISPATCH_QUEUE_CONCURRENT
指定为并发队列
对于串行队列,GCD提供了一种特殊的串行队列:主队列(Main Dispatch Queue)
- 所有放在主队列中的任务,都会放到主线程中执行
- 可使用
dispatch_get_main_queue()
方法获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();
对于并发队列,GCD默认提供了:全局并发队列(Global Dispatch Queue)
- 可以使用
dispatch_get_global_queue
方法来获取全局并发队列。需要传入两个参数,第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT
。第二个一般用0.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
任务的创建方法
GCD提供了同步执行任务的创建方法dispatch_sync
和异步执行任务创建方法dispatch_async
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});
虽然创建任务只需两步,但是既然我们有两种队列(串行队列/并发队列),两种任务执行方式(同步执行/异步执行)
所以总共有四个组合
- 同步+并发
- 异步+并发
- 同步+串行
- 异步+串行
实际上还有两种特殊队列:全局并发队列、主队列。
全局并发队列可以作为普通的并发队列
主队列就不一样了
- 同步执行+主队列
- 异步执行+主队列
不同的组合有什么区别?
任务和队列不同组合方式的区别
先来考虑最简单的使用情况
主线程下 不同队列+不同任务 组合使用的不同区别 暂时不考虑队列嵌套队列这种复杂情况
主线程中,不同队列+不同任务简单组合的区别:
区别 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步 | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 死锁,卡住不执行 |
异步 | 开启新线程,并发执行任务 | 开启一条新线程,串行执行任务 | 没有开启新线程,串行执行任务 |
从表格可以看出 主线程中调用主队列+同步执行会导致死锁的问题
这是因为 主队列中追加的同步任务和主线程本身的任务两者之间相互等待,阻塞了主队列,最终造成了主队列的死锁
我们在其他线程调用主队列+同步执行,则不会阻塞主队列,自然不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务
队列嵌套情况下,不同组合的区别
除了上面在【主线程】中调用【主队列】+【同步执行】会导致死锁问题。实际在使用【串行队列】的时候,也可能出现阻塞【串行队列】所在线程的情况。比如同一个串行队列的嵌套使用。
在【异步执行】 + 【串行队列】的任务中,又嵌套了【当前的串行队列】,然后进行【同步执行】
执行这个代码会导致串行队列中追加的任务和串行队列中原有的任务两者之间相互等待,阻塞了【串行队列】,造成了串行队列所在线程(子线程)死锁问题
不同队列+不同任务的组合,以及【队列中嵌套队列】的区别 后面细讲 这里先扔个结论
关于不同队列和不同任务的理解
现在假设有5个人要穿过一个门禁,这个门禁总共有10个入口,管理员可以决定同一时间打开几个如果,可以决定同一时间让一个人单独通过还是多个人一起通过。默认情况下,管理员只开启一个入口,且一个通道一次仅仅通过一个人。
我们以这个为例子,人好比是任务,管理员好比是系统,入口则代表线程。
5个人表示5个任务
10个人表示10条线程
- 串行队列就是5个人排成一支长队
- 并发队列就是5个人拍成不止一支队伍,可能2,3队
- 同步任务就是管理员只开启了一个入口(当前线程)
- 异步任务就是管理员同时开启了多个入口(当前线程+新开的线程)
- 异步+并发: 管理员开了多个入口(假设为3个),5个人排成了多只队伍(假设3个队伍),这样5个人就可以3个人同时一起穿过门禁
- 同步+并发: 同步这就开了一个入口,虽然并发执行有多只队伍,但是一个入口一次只能通过1个人,所以只好一个一个的走过去,表现的结果就是顺利通过入口
所以换成GCD来说
- 异步+并发:系统开启了多个线程,任务可以多个同时运行
- 同步+并发:系统默认开了一个主线程,没有开启子线程,虽然处于并发队列中,但也只能一个接一个执行
GCD的基本使用
开干!
同步+并发
特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务
相当于开了一个门,虽然有多只队伍,但是一个如果一次只能通过一个人,所以只好一个一个的走过去
我们新建三个同步执行的任务 每个都有模拟2秒的耗时 并且打印其所在线程
- (void)syncConcurrent {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncConcurrent---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"syncConcurrent---end");
}
结果如下
- 我们可以看到所有的任务都是在当前主线中执行的,并没有开启新线程。(同步执行没有开启新线程的能力)
- 所有任务都在begin—end之间执行的(同步任务需要等待队列的任务执行结束)
- 任务是按照顺序执行的。虽然并发可以开启多个线程,并且同时执行多个任务,但是因为其本身不能创建新线程,所以不存在并发。同步又要求等在当前队列中的任务只能一个一个执行,不能被同时执行。
异步+并发
- (void)asyncConcurrent {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"asyncConcurrent---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"asyncConcurrent---end");
}
- 除了当前线程(主线程),系统又开启了3个线程,任务是交替/同时进行的(异步具备开启新线程的能力。并发可开启多个线程,同时执行多个任务)
- 所有任务是在打印
begin----end
之后才执行的。说明当前没有等待,而是直接开启了新线程,在新线程中执行任务。(异步执行,不会做任何的等待,可以继续执行任务)
同步+串行
不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
- (void)syncSerial {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncSerial---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"syncSerial---end");
}
结果和同步+并发一样,我们很清楚的可以明白,虽然上面那个叫并发,但是同步把其进行了限制,结果和同步串行一样
异步+串行
会开启新线程,但因为任务是串行的,执行完一个任务再执行下一个任务
- (void)asyncSerial {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"asyncSerial---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"asyncSerial---end");
}
- 开启了一条新线程(异步具备开启新线程的能力,串行仅仅开启了一个线程 相当于异步强行搞出来了新线程,但是串行保证其仅仅就是一个新线程,这是和并行不一样的)
同步+主队列
相互卡住不可执行
- (void)syncMain {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncMain---begin");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"syncMain---end");
}
直接crash了
这是因为我们在主线程中直接执行同步+主队列这个函数
相当于把这个函数放到了主线程的队列中。而同步执行会等待当前队列中的任务执行完毕才会接着执行。当我们把【任务1】追加到主队列中,【任务1】就在等待主线程处理完syncMain
任务。而syncMain
任务需要等待【任务1】执行完毕,才能接着执行。
syncMain
和【任务1】都在等待对方执行完毕。大家相互等待,造成死锁
并且syncMain—end也没有被打印
在其他线程中调用同步+主队列
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
没有开启新线程,所有任务是按顺序执行
那么为什么在其他线程调用就是正确的呢,不会产生死锁呢
因为syncMain
放到了其他线程,而【任务1】、【任务2】、【任务3】都在追加到主队列中,这三个任务都会在主线程中执行。syncMain
任务在其他线程中执行到追加【任务1】到主队列中,主队列现在没有正在执行的任务,所以,会直接执行主队列的【任务1】,等【任务1】执行完毕,再接着执行【任务2】、【任务3】。所以这里不会卡住线程,也就不会造成死锁问题。
异步+主队列
只在主线程中执行任务,执行完一个任务,再执行下一个任务
- (void)asyncMain {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"asyncMain---begin");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"asyncMain---end");
}
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但是因为是主队列,所以所有的任务都在主线程中)
- 任务是按顺序是行的,并且所有任务是在打印
begin---end
之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)
GCD其他方法
栅栏方法:dispatch_barrier_async
- 我们有时候需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作,当然操作组里也可以包含一个或者多个任务。
- 这就需要用到
dispatch_barrier_async
方法在两个操作组间形成栅栏 dispatch_barrier_async
方法会等待前边追加到并发队列中的任务全部执行完毕后,再将制定的任务追加到该异步队列中。然后在dispatch_barrier_async
方法追加的任务执行完毕之后,接着追加任务到该异步队列并开始执行
dispatch_queue_t queue = dispatch_queue_create("net.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_barrier_async(queue, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
});
- 在执行完栅栏前面的操作之后执行栅栏操作,最后再执行栅栏后面的操作
GCD中同步栅栏和异步栅栏
我们考虑先后顺序应该是以Block为整体的
之前也仅仅考虑异步栅栏+单一队列这个问题
栅栏作用于同一队列
那么对于身处不同队列又有什么样的拦截作用
异步栅栏+单一队列
同步栅栏+单一队列
就是同步栅栏单纯的阻塞队列 异步栅栏不阻塞队列
同时队列要是串行队列,就一个接一个的执行,要是并发队列,执行顺序就会发生改变
很好理解
异步栅栏+多队列
纯乱序了,完全不确定,大伙都在异步执行,执行顺序都是交替的
同步栅栏+多队列
1、2和栅栏是交叉打印,他们没有先后顺序,同时因为同步栅栏的作用导致阻塞了主线程,不能进行后面的操作,所以aaa和4、5都要在栅栏结束后才打印
延时执行方法:dispatch_after
在Block的强弱共舞的例子有学过
需要注意的是:dispatch_after方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。所以严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after方法还是很有效的
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
// 在3秒后追加Block到Main Dispatch Queue中
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"waited at least three seconds");;
});
dispatch_after函数
第一个参数:
指定时间的dispatch_time_t类型的值,可以使用dispatch_time函数或dispatch_walltime函数作成。
第二个参数:
要追加处理的Dispatch Queue。
第三个参数:
要执行处理的Block。
dispatch_time这个函数
第一个参数:
指定时间开始
第二个参数:
指定时间后的时间,通常用于计算相对时间。DISPATCH_TIME_NOW 表示现在的时间。
“ull”是C语言的数值字面量,是显示表明类型时使用的字符串(unsign long long)。
SEC 秒
PER 每
NSEC 纳秒
MSEC 毫秒
USEC 微秒
我们往往使用3ull * NSEC_PER_SEC这种方式来进行计算
因为官方文档要求我们这个具体的事件使用的单位是纳秒
一次性代码(只执行一次):dispatch_once
我们在创建单例、或者有整个程序运行过程中只执行一次的方法时,我们就用到了GCD的dispatch_once
方法。使用dispatch_once
方法能保证某段代码在程序运行过程中只被执行了一次,并且即使在多线程的环境下,dispatch_once
也可以保证线程安全
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行 1 次的代码(这里面默认是线程安全的)
});
快速迭代方法:dispatch_apply
我们通常是使用for循环遍历的方法,但是GCD给我们提供了快速迭代的方法dispatch_apply
。
其按照指定的此处将指定的任务追加到指定的队列中,并等待全部队列执行结束。
如果在串行队列中使用dispatch_apply
,那么就和for循环一样,按顺序同步执行。但这样快速迭代的意义就没有体现出来。
我们可以利用并发队列进行异步执行。比如说遍历0~5这6个数字,for循环的做法是每次取出一个元素进行逐个遍历,但是dispatch_apply
可以在多个线程中同时(异步)遍历多个数字
还有一点,无论是在串行队列还是并发队列中,dispatch_apply都会等待全部任务执行完成,这点就像是同步操作,也像是队列组中的dispatch_group_wait
方法。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//全局并发队列
NSLog(@"apply---begin");
dispatch_apply(6, queue, ^(size_t index) {
NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
/*
第一个参数表示重复次数
第二个参数表示将要执行处理的queue
第三个参数:要执行的处理,带有参数,第多少次,是为了按第一个参数重复追加Block并区分各个Block而使用的。
*/
这里不需要考虑同步不同步的问题 其仅仅就是遍历队列
因为是在并发队列中异步执行任务 所以各个任务的执行时间长短不定,最后的结束顺序也不能确定,但是一定在begin—end中,因为dispatch_apply
方法会等待全部任务执行完毕
队列组:dispatch_group
分别异步执行两个耗时任务,然后当两个耗时任务都执行完毕后再回到主线程执行任务。这就是我们用到队列组的情况
- 调用队列组的
dispatch_group_async
先把任务放到队列中,然后将队列放入队列组中。或者 使用dispatch_group_enter
、dispatch_group_leave
组合来实现dispatch_group_async
。 - 调用队列组的
dispatch_group_notify
回到指定线程执行任务。或者使用dispatch_group_wait
回到当前线程继续向下执行(会阻塞当前线程)
dispatch_group_notify
监听group中任务的完成状态,当所有的任务都执行完成后,追加任务到group中,并执行任务
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
- dispatch_group_create函数生成dispatch_group_t类型的group
- dispatch_group_async函数,第一个参数为生成的group,第二个参数为队列,第三个参数是Block
- dispatch_group_notify函数第一个参数为需要监视的group,第二个参数为要追加结束处理的队列,第三个结束对于我们追加的队列进行Block的处理
当所有任务都执行完成之后,才执行notify相关block中的任务
dispatch_group_wait
暂停当前线程,等待指定的group中的任务完成后,才会继续往下执行
- (void)groupWait {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
// 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"group---end");
NSLog(@"currentThread---%@",[NSThread currentThread]);
}
- dispatch_group_wait函数的第一个参数为等待的Dispatch Group,第二个参数为等待的时间,就是dispatch_time_t类型
wait会暂停现在的线程,去执行wait中的线程,wait中的线程执行结束后,才会执行其之后的操作(或者秒数读完了)。所以使用dispatch_group_wait
会造成当前线程的阻塞
dispatch_group_enter、dispatch_group_leave
这个相当于我们使用dispatch_group_enter、dispatch_group_leave来实现了dispatch_group_async的功能
dispatch_group_enter
标志着一个任务追加到了group,执行一次,相当于group中未执行完毕任务数+1。dispatch_group_leave
标志着一个任务离开了group,执行一次,相当于group中未执行完毕任务数-1。- 当group中未执行完毕任务数为0的时候,才会使
dispatch_group_wait
解除对当下线程的阻塞,以及执行追加到notify中的任务
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
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, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程.
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
其实就是等同于使用dispatch_group_async
里面只有一个参数就是group
dispatch_group_enter(group);
dispatch_group_leave(group);
信号量:dispatch_semaphore
dispatch_semaphore_create
:创建一个Semaphore并初始化信号的总量dispatch_semaphore_signal
: 发送signal信号,信号量+1dispatch_semaphore_wait
:使信号量-1- (原来在iOS也逃不过PV操作😢)信号量的计数值小于0时等待 (阻塞),大于等于0时正常执行。(官方注释:如果结果值小于零,此函数在返回前等待信号出现。)所以和操作系统学习的信号量的判读还不太一样
dispatch Semaphore在实际开发中主要用于
- 保持线程同步,将异步执行的操作转换为同步执行的任务
- 保证线程安全,为线程加锁
dispatch Semaphore线程同步(异步转同步)
常常会遇到:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话来说,相当于,将异步执行任务转换为同步执行任务。
- (void)semaphoreSync {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
number = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
}
可以看到,是在semaphore—end是在number = 100之后才打印的。而且输出结果number为100。还是那句话【异步执行】不会做任何等待,可以继续执行任务。
- semaphore初始创建时引用计数为0
- 异步执行将任务1追加到队列之后,不做等待,接着执行
dispatch_semaphore_wait
方法,semaphore减一,此时semaphore == -1
,当前线程进入等待状态 - 然后,异步任务1开始执行。任务1执行到
duspatch_semaphore_signal
之后,信号量加了个1,semaphore == 0
,正在被阻塞的线程(主线程)恢复继续执行 - 最后打印end, number == 100
我们如果不使用信号量来控制同步,异步执行不会做任何等待,继续创建任务
block依旧保持0
如此便实现了线程同步,将异步转成同步的过程(函数中的BLock变量仅仅是用来判断执行先后顺序的变量)
线程安全与线程同步(为线程+锁)
我们在atomic和nonatomic中已经清楚的知道什么是线程的不安全
不考虑线程安全(不使用Semaphore)
- (void)initTicketStatusNotSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
self.ticketSurplusCount = 20;
//假设一共有20张火车票,1和2表示两个窗口
// queue1 代表售卖窗口1
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表售卖窗口2
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketNotSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketNotSafe];
});
}
/**
* 售卖火车票(非线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
if (self.ticketSurplusCount > 0) { //如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", (long)self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"已售完");
break;
}
}
}
在分析这个代码之前,我们回到最开始 毫无疑问 串行+异步
不过与上面不同的是,这里创建了两个队列 所以串行+异步的结果和并行+异步的结果一样,创建了两个currentThread都是新线程
同理 两个队列在同步的条件下 还是没有开启新线程,串行的执行任务
回到上面这个代码 我们没有考虑线程安全的问题
此时的结果
票数是混乱的 并不能确定两个线程在买票时是否与另一个线程相绑定
加锁来确保线程安全
还是上面那个例子
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
semaphoreLock = dispatch_semaphore_create(1);
self.ticketSurplusCount = 20;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", (long)self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
}
}
从结果看很完美 看一下具体的实现思路
- 我们先把信号量设置为1,假设第一个线程进入sale方法时,调用wait减一,信号量为0,正常执行,不需要阻塞。
- 在此时,第二个线程又需要进入sale方法,此时第一个线程的操作还没有结束,wait操作从而导致此时信号量小于了0,所以阻塞等待信号量正常才继续执行。
- 在第一个线程卖完票后,signal是信号量又变为了0,第二个等待的线程现在开始执行。
所以在这里相当于售票是个临界区,我们需要对临界区进行互斥访问,一次仅允许进入一个线程,有点类似于临界区资源打印机的那个过程,我们通过PV操作来避免这个情况
PS:
这个github的demo太舒服了
永远滴神
问题
之前没有考虑循环创建异步任务这部分的内容
先说为什么会报错
因为其是异步的 我们不能确定什么时候开始执行某个异步线程 所以当两个异步线程刚好同时往数组里添加元素时就会报错
但这个问题也让我学到了 并不是每个分配一个线程,就像for循环十个,不会创建十个线程,在循环中,前面用完的可以给后面用。线程是系统管理的,系统会根据需要创建。
我们在循环中尝试打印一下每个所属的线程
我们看到确实不是为每个都新创建,而是由系统管控,可能for循环第八次的时候 创建的第一个线程空闲了 就会交给第一个线程去处理
什么是线程空闲了呢 完全由系统进行判断吗 完全随机吗?明天再说吧
- 循环嵌套导致的死锁问题未解决—仅仅知道结果
- GCD的任务执行顺序
dispatch_queue_t queue = dispatch_queue_create("qwe", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
});
NSLog(@"3");
dispatch_sync(queue, ^{
NSLog(@"4");
});
NSLog(@"5");
结果是13245
我们来理一下思路
先打印1
接下来将2添加到串行队列上,因为2是异步执行,不会阻塞线程,继续向下执行,打印3。
然后是将4添加到串行队列上,4必须要等2执行后才能继续执行,又因为4是同步任务,会阻塞线程,只有完成4才能继续执行5.
所以1 3 2 4 5
此时线程4在主线程执行,因为同步线程没有开启新线程的能力
此时我们去掉同步的任务
换成NSLog(@“6”);
那么就一个异步执行的任务 我们无法知道其执行的实际和结束的时机,所以每次的打印结果可能都不一样