首先看一下iOS中多线程的实现方案
- 这些多线程方案的底层都是依赖pthread
- NSThread线程生命周期是程序员管理,GCD和NSOperation是系统自动管理
- NSThread和NSOperation都是OC的,更加面向对象
- NSOperation基于CGD,使用更加面向对象
一. GCD的简单使用
1. 同步、异步、串行、并发
先理解比较容易混淆的术语:同步、异步、串行、并发
同步(sync)和异步(async)主要影响:能不能开启新的线程
同步(sync):在当前线程中执行任务,不具备开启新线程的能力
异步(async):在新的线程中执行任务,具备开启新线程的能力
并发和串行主要影响:任务的执行方式
串行:一个任务执行完毕后,再执行下一个任务
并发:多个任务并发(同时)执行
2. GCD常用函数
GCD是开源的,源码在:https://github.com/apple/swift-corelibs-libdispatch
GCD中有两个用来执行任务的函数,queue是队列,block是要执行的任务,如下:
- 用同步的方式执行任务:
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
- 用异步的方式执行任务:
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_sync不具备开启线程的能力,它的任务在当前线程执行的。
dispatch_async具备开启线程的能力,它的任务在子线程执行的。
3. GCD的队列
GCD的队列可以分为两大类型
- 串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务) - 并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效
总结:
dispatch_sync和dispatch_async函数决定是否在当前线程执行,串行队列和并发队列决定任务是串行执行还是并发执行,他们决定的东西互不影响。
4. GCD函数和队列的组合
先看结论:
全局并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 没有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
死锁 |
异步(async) | 有开启新线程 并发执行任务 |
有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
① 全局并发队列
- (void)viewDidLoad
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1在线程%@执行",[NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2在线程%@执行",[NSThread currentThread]);
}
});
}
打印:
任务2在线程{number = 1, name = main}执行
任务1在线程{number = 3, name = (null)}执行
任务2在线程{number = 1, name = main}执行
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务1在线程{number = 3, name = (null)}执行
任务2在线程{number = 1, name = main}执行
任务1在线程{number = 3, name = (null)}执行
可以发现,任务1和任务2同时执行,任务1在子线程执行,任务2在当前主线程执行,任务1和任务2是并发的。
② 串行队列
- (void)viewDidLoad
{
NSLog(@"任务000");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1在线程%@执行",[NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2在线程%@执行",[NSThread currentThread]);
}
});
NSLog(@"任务333");
}
打印:
NSLog(@"任务000");
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务1在线程{number = 3, name = (null)}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
NSLog(@"任务333");
可以发现,任务1先执行,任务2后执行,任务1在子线程执行,任务2在当前主线程执行,任务1和任务2是串行的。
注意:虽然上面有使用create,但是在ARC中不用写dispatch_release(queue),因为ARC已经帮你做过了(CF开头的该写还是要写)。
③ 主队列(是一种特殊的串行队列)
- (void)viewDidLoad
{
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1在线程%@执行",[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2在线程%@执行",[NSThread currentThread]);
}
});
}
打印:
任务1在线程{number = 1, name = main}执行
任务1在线程{number = 1, name = main}执行
任务1在线程{number = 1, name = main}执行
任务1在线程{number = 1, name = main}执行
任务1在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
任务2在线程{number = 1, name = main}执行
注意:上面都是使用dispatch_async函数,如果在主队列中使用dispatch_sync函数会产生死锁,报错:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
上面说了dispatch_async具备开启新线程的能力,但是并不代表它一定要开启新线程。比如上面,使用dispatch_async传入主队列,因为放到主队列里面的所有东西都会在主线程执行,所以就没开启新线程。
可以发现,任务1先执行,任务2后执行,任务1和任务2都在主线程执行,任务1和任务2是串行的。
5. 关于死锁
首先要明白两个特点:
- 队列的特点:FIFO (First In First Out),先进先出。
- dispatch_sync函数的特点:sync函数要求立马在当前线程同步执行任务。
先看结论:
- 使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
- 两个关键点:sync函数,当前串行队列
- 上面②中,没有产生死锁,是因为使用sync函数添加任务到自己创建的串行队列中了,并没有添加到当前的串行队列(主队列),所以不会产生死锁。
- 上面③中,如果把其中一个async换成sync就会产生死锁,因为主队列是当前串行队列,使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列,也就是会卡住主队列。
- 下面问题1,同上面4
- 下面问题3,当前串行队列是myqueu,再往myqueu队列里面添加sync任务,就会卡住myqueu队列。
问题1:
以下代码是在主线程执行的,会不会产生死锁?会!
- (void)viewDidLoad
{
NSLog(@"执行任务1%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
//报错:Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
NSLog(@"执行任务2%@",[NSThread currentThread]);
});
NSLog(@"执行任务3%@",[NSThread currentThread]);
}
打印:
2020-09-17 10:05:57.539001+0800 GCD[49304:3047986] 执行任务1{number = 1, name = main}
(lldb)
可能你会想dispatch_sync不具备开启新线程的能力,那就在主线程执行,正好传入的是主队列,那就先执行任务1再执行任务2再执行任务3不就好了,而实际情况为什么会死锁呢?
死锁的原因:由于队列先进先出,要想执行任务3,就要任务2先执行完。要想执行任务2,就要viewDidLoad这个大任务先执行完,viewDidLoad要执行完,就要等任务3先执行完。最后任务3等任务2,任务2等任务3,他们就这样互相卡住,产生死锁。
图示如下:
问题2:
以下代码是在主线程执行的,会不会产生死锁?不会!
- (void)viewDidLoad
{
NSLog(@"执行任务1%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务2%@",[NSThread currentThread]);
});
NSLog(@"执行任务3%@",[NSThread currentThread]);
}
dispatch_async函数特点:不要求立马在当前线程同步执行任务
打印结果:
执行任务1{number = 1, name = main}
执行任务3{number = 1, name = main}
执行任务2{number = 1, name = main}
可以发现,任务1 3 2按顺序在主线程执行,任务2会在任务3执行完成后再执行,自己可分析上图理解。
问题3:
以下代码是在主线程执行的,会不会产生死锁?会!
- (void)viewDidLoad
{
NSLog(@"执行任务1%@",[NSThread currentThread]);
//创建串行队列
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
//下面这个dispatch_async函数,我们把它称为大任务
dispatch_async(queue, ^{ // block0
NSLog(@"执行任务2%@",[NSThread currentThread]);
dispatch_sync(queue, ^{ // block1
NSLog(@"执行任务3%@",[NSThread currentThread]);
});
NSLog(@"执行任务4%@",[NSThread currentThread]);
});
NSLog(@"执行任务5%@",[NSThread currentThread]);
}
分析代码,首先肯定会先在主线程执行任务1,然后大任务是在子线程串行执行,任务5在主线程串行执行,所以打印结果:
可能是:
执行任务1{number = 1, name = main}
执行任务5{number = 1, name = main}
执行任务2{number = 3, name = (null)}
(lldb)
也可能是:
执行任务1{number = 1, name = main}
执行任务2{number = 3, name = (null)}
执行任务5{number = 1, name = main}
(lldb)
这取决于任务5和子线程的大任务谁先执行,是不确定的。
现在就知道为什么死锁了,首先外面的大任务先称为block0,里面的小任务先称为block1。系统会先把block0添加到串行队列中,再把block1添加到串行队列中。先执行完block0,但是block0要执行完就要执行block1,但是block1是后添加的,执行block1要先执行完block0,他们就这样卡着,产生死锁。
图示如下:
问题4:
以下代码是在主线程执行的,会不会产生死锁?不会!
- (void)viewDidLoad
{
NSLog(@"执行任务1%@",[NSThread currentThread]);
//创建串行队列
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
//创建全局并发队列
dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
//创建串行队列
//dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // block0
NSLog(@"执行任务2%@",[NSThread currentThread]);
dispatch_sync(queue2, ^{ // block1
NSLog(@"执行任务3%@",[NSThread currentThread]);
});
NSLog(@"执行任务4%@",[NSThread currentThread]);
});
NSLog(@"执行任务5%@",[NSThread currentThread]);
}
分析代码,首先肯定在主线程执行任务1,之后block0会在子线程执行,任务5会在主线程执行,但是block0和任务5谁先执行说不准。
当执行到block0的时候,执行到任务3会先去另外一个串行队列执行block1,之后再执行任务4。虽然和上面那个死锁代码差不多,但是因为他们不在同一个队列,所以不会死锁,图示如下:
打印结果:
可能是:
执行任务1{number = 1, name = main}
执行任务5{number = 1, name = main}
执行任务2{number = 3, name = (null)}
执行任务3{number = 3, name = (null)}
执行任务4{number = 3, name = (null)}
也可能是:
12534
12354(需要打断点才可以打印出来)
12345
同理,如果把上面的queue2换成串行队列,这时候还是不会产生死锁,图示如下:
问题5:
以下代码是在主线程执行的,会不会产生死锁?不会!
- (void)viewDidLoad
{
NSLog(@"执行任务1%@",[NSThread currentThread]);
//创建全局并发队列
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // block0
NSLog(@"执行任务2%@",[NSThread currentThread]);
dispatch_sync(queue, ^{ // block1
NSLog(@"执行任务3%@",[NSThread currentThread]);
});
NSLog(@"执行任务4%@",[NSThread currentThread]);
});
NSLog(@"执行任务5%@",[NSThread currentThread]);
}
这时候,block0和block1都在全局并发队列里面,执行到任务3要执行block1,既然要执行block1那就执行吧,反正任务是并发,可以同时执行。
如下图:
任务1 5在主线程执行,任务2 3 4在同一个子线程执行,任务1最先执行,任务2 3 4按顺序执行,任务5穿插其中,所以有四种情况,如下:
//执行任务1{number = 1, name = main}
//执行任务5{number = 1, name = main}
//执行任务2{number = 5, name = (null)}
//执行任务3{number = 5, name = (null)}
//执行任务4{number = 5, name = (null)}
//执行任务1{number = 1, name = main}
//执行任务2{number = 3, name = (null)}
//执行任务3{number = 3, name = (null)}
//执行任务4{number = 3, name = (null)}
//执行任务5{number = 1, name = main}
//执行任务1{number = 1, name = main}
//执行任务2{number = 6, name = (null)}
//执行任务3{number = 6, name = (null)}
//执行任务5{number = 1, name = main}
//执行任务4{number = 6, name = (null)}
//执行任务1{number = 1, name = main}
//执行任务2{number = 5, name = (null)}
//执行任务5{number = 1, name = main}
//执行任务3{number = 5, name = (null)}
//执行任务4{number = 5, name = (null)}
补充:获取的全局并发队列和创建的全局并发队列的区别
- (void)viewDidLoad
{
dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue3 = dispatch_queue_create("queu3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue4 = dispatch_queue_create("queu4", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"%p %p %p %p", queue1, queue2, queue3, queue4);
}
打印:
0x10db71e80 0x10db71e80 0x600000f81480 0x600000f81400
可以发现:
- 两种方式都可以获取全局并发队列
- 使用系统获取的一直是同一个全局并发队列,自己创建的每个全局并发队列都不一样
6. 面试题
1. 下面代码打印什么?
- (void)test2
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
// 这句代码的本质是往Runloop中添加定时器
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}
- (void)test
{
NSLog(@"2");
}
运行后发现打印:1 3。
为什么打印:1 3,不打印2,为什么?
因为performSelector: withObject: afterDelay:的本质是往Runloop中添加定时器,属于Timers事件,子线程默认没有开启RunLoop,所以定时器不会执行,所以打印1 3。
可能有人会问,子线程的Runloop没有开启,那么为什么同样是在子线程的1 3可以打印呢?
其实1 3和子线程的RunLoop没有关系,1 3是普通的代码,会立马执行,2是定时器,会添加到RunLoop中。
如果把子线程的RunLoop运行起来:
- (void)test2
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
// 这句代码的本质是往Runloop中添加定时器,属于Timers事件。
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"4");
});
}
- (void)test
{
NSLog(@"2");
}
打印:1 3 2,这时候定时器添加RunLoop了,test方法会在下一次RunLoop循环中处理,所以打印结果是1 3 2。又因为RunLoop添加了addPort这时候RunLoop会一直在工作,所以不会走到4。
如果把上面代码:[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]注释掉,会打印:1 3 2 4,这是因为RunLoop执行完2的定时器事件就没事了,RunLoop就会退出,这样就会执行4。
同理,如果将上面代码在主线程里面执行:
- (void)test2
{
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"1");
// 这句代码的本质是往Runloop中添加定时器,属于Timers事件。
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}
- (void)test
{
NSLog(@"2");
}
打印:1 3 2,因为主线程的RunLoop默认自动开启的,所以会打印1 3 2。
如果将performSelector:withObject:afterDelay:换成performSelector:withObject:呢?
我们在NSObject.mm里面查看performSelector:withObject:afterDelay:源码:
- (id)performSelector:(SEL)sel withObject:(id)obj {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}
可以发现performSelector:withObject:的内部直接就是objc_msgSend,所以就相当于方法调用,和RunLoop无关,所以就一行一行执行就好了。
objc4源码里面没找到performSelector:withObject:afterDelay:的内部实现,只知道它定义在RunLoop.h里面了,说明它是RunLoop的东西。
如果想要看performSelector:withObject:afterDelay:的底层实现要先了解一下GNUstep。
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
源码地址:http://www.gnustep.org/resources/downloads.php
虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值
在上面地址找到GNUstep Base,发现它是Foundation框架的,如下图:
找到Foundation框架下的NSRunLoop.m文件,搜索“performSelector”,找到如下实现:
- (void) performSelector: (SEL)aSelector
withObject: (id)argument
afterDelay: (NSTimeInterval)seconds
{
NSRunLoop *loop = [NSRunLoop currentRunLoop];
GSTimedPerformer *item;
item = [[GSTimedPerformer alloc] initWithSelector: aSelector
target: self
argument: argument
delay: seconds];
[[loop _timedPerformers] addObject: item];
RELEASE(item);
[loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}
可以发现,首先创建一个RunLoop,再创建一个定时器,再把定时器添加到RunLoop里面。可以知道,performSelector:withObject:afterDelay:里面是有创建RunLoop的,但是没有运行RunLoop,所以上面的代码才会那样。
还找到run方法的内部实现:
- (void) run
{
[self runUntilDate: theFuture];
}
- (void) runUntilDate: (NSDate*)date
{
BOOL mayDoMore = YES;
/* Positive values are in the future. */
while (YES == mayDoMore)
{
mayDoMore = [self runMode: NSDefaultRunLoopMode beforeDate: date];
if (nil == date || [date timeIntervalSinceNow] <= 0.0)
{
mayDoMore = NO;
}
}
}
可以发现run方法内部的确使用while循环一直在调用runMode:beforeDate:,这和线程保活里面说的一致。
总结:
- performSelector:withObject:的内部直接就是objc_msgSend
- performSelector:withObject:afterDelay:的内部会创建一个RunLoop,再创建一个定时器,再把定时器添加到RunLoop里面,但是并没有运行RunLoop
- run方法内部的确使用while循环一直在调用runMode:beforeDate:
2. 下面代码打印什么?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test
{
NSLog(@"2");
}
运行代码,打印1之后报错:
1
-[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform'
翻译过来:当执行performSelector的时候,目标线程已经退出。
这是因为 [thread start]之后就会执行block,然后打印1,打印完成线程就退出了,这时候再去这个线程上做事情就会报上面的错。
解决办法如下,给这个线程添加RunLoop,再给RunLoop添加端口事件,让RunLoop一直有事做,这样线程就不会退出了,如下:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test
{
NSLog(@"2");
}
上面代码添加RunLoop之后,线程就一直有事情做了。当打印完1,线程就开始休眠,这时候执行performSelector方法,就是给线程发送一个消息,然后线程被唤醒,开始执行test方法。运行代码,打印:1 2。
二. 延迟执行
iOS常见的延时执行有两种方式:
1. 调用NSObject的方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// 2秒后再调用self的run方法
该方法在哪个线程调用,那么run就在哪个线程执行,通常是主线程,代码如下:
@implementation YYViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"打印线程----%@",[NSThread currentThread]);
//在主线程中执行run方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
}
- (void)run
{
NSLog(@"延迟执行----%@",[NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
dispatch_queue_t queue = dispatch_queue_create("wendingding", 0);
//在子线程中执行test方法
dispatch_async(queue, ^{
[self performSelector:@selector(test) withObject:nil afterDelay:1.0];
});
NSLog(@"异步函数");
}
- (void)test
{
NSLog(@"子线程中执行----%@",[NSThread currentThread]);
}
@end
2. 使用dispatch_after函数
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后异步执行这里的代码...
});
传入一个队列,如果是主队列,那么就在主线程执行,如果是并发队列,那么会新开启一个线程,在子线程中执行,代码如下:
@implementation YYViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"打印当前线程---%@", [NSThread currentThread]);
//传入主队列,在主线程执行
dispatch_queue_t queue= dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), queue, ^{
NSLog(@"主队列--延迟执行------%@",[NSThread currentThread]);
});
//全局并发队列,在子线程执行
//1.获取全局并发队列
dispatch_queue_t queue1= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2.计算任务执行的时间
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
//3.会在when这个时间点,执行queue中的这个任务
dispatch_after(when, queue1, ^{
NSLog(@"并发队列-延迟执行------%@",[NSThread currentThread]);
});
}
@end
三. 一次性代码
使用dispatch_once函数能保证某段代码在程序运行过程中只被执行一次,如下:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
四. GCD队列组
思考:如何用GCD实现以下功能?
异步并发执行任务1、任务2,等任务1、任务2都执行完毕后,再回到主线程执行任务3。
代码如下:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
// 添加异步任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1-%@", [NSThread currentThread]);
}
});
// 添加异步任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2-%@", [NSThread currentThread]);
}
});
// 等前面的任务执行完毕后,会在主线程自动执行这个任务
dispatch_group_notify(group, queue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
});
}
或者:
// 等前面的任务执行完毕后,会回到主线程自动执行这个任务
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
打印:
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务3-{number = 1, name = main}
任务3-{number = 1, name = main}
任务3-{number = 1, name = main}
任务3-{number = 1, name = main}
任务3-{number = 1, name = main}
可以发现,先在子线程异步执行任务1和任务2,任务1和任务2都执行完成后,再回到主线程执行任务3。
如果有两个dispatch_group_notify呢?
// 等前面的任务执行完毕后,会在异步线程,异步执行这两个任务
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务4-%@", [NSThread currentThread]);
}
});
打印:
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务1-{number = 3, name = (null)}
任务2-{number = 4, name = (null)}
任务3-{number = 4, name = (null)}
任务4-{number = 3, name = (null)}
任务4-{number = 3, name = (null)}
任务3-{number = 4, name = (null)}
任务4-{number = 3, name = (null)}
任务3-{number = 4, name = (null)}
任务4-{number = 3, name = (null)}
任务3-{number = 4, name = (null)}
任务4-{number = 3, name = (null)}
任务3-{number = 4, name = (null)}
可以发现,如果有两个dispatch_group_notify,任务1和任务2在子线程异步执行完成后,还会在子线程异步执行任务3和任务4。
总结:
使用GCD队列组,dispatch_group_notify(group, queue, ^{}) 函数会在group里面的任务执行完再执行,如果queue是主队列就在主线程执行这个函数的任务,如果queue是全局并发队列,就在子线程异步执行这个函数的任务。
Demo地址:GCD