文章目录
- iOS - 多线程-GCD
- 1. 常见多线程方案
- 2. GCD
- 2.1 GCD的常见函数
- GCD中有2个用来执行任务的函数
- 2.2 GCD的队列
- 2.2.1 GCD的队列可以分为2大类型
- 2.3 容易混淆的术语
- 2.4.1 有4个术语比较容易混淆:`同步、异步`、`并发、串行`
- 2.4 各种队列的执行效果
- 3. 死锁
- 3.1 死锁示例
- 3.2 死锁分析
- 3.3 其他示例
- 3.3.1 interview02
- 3.3.2 interview03
- 3.3.3 interview04
- 4. 案例
- 4.1 案例1
- 4.2 分析
- 5. 拓展
- GNUstep
iOS - 多线程-GCD
1. 常见多线程方案
NSThread
、GCD
、NSOperation
底层都依赖于pthread
2. GCD
2.1 GCD的常见函数
GCD中有2个用来执行任务的函数
-
用
同步
的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);- queue:队列
- block:任务
-
用
异步
的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block); -
GCD源码:https://github.com/apple/swift-corelibs-libdispatch
2.2 GCD的队列
2.2.1 GCD的队列可以分为2大类型
-
并发
队列(Concurrent Dispatch Queue)- 可以让多个任务
并发(同时)
执行(自动开启多个线程同时执行任务) 并发
功能只有在异步
(dispatch_async
)函数下才有效
- 可以让多个任务
-
串行
队列(Serial Dispatch Queue)- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
2.3 容易混淆的术语
2.4.1 有4个术语比较容易混淆:同步、异步
、并发、串行
-
同步
和异步
主要影响:能不能开启新的线程 -
同步
:在当前
线程中执行任务,不具备
开启新线程的能力
-
异步
:在新的
线程中执行任务,具备
开启新线程的能力
-
并发
和串行
主要影响:任务的执行方式 -
并发
:多个
任务并发(同时)执行
-
串行
:一个
任务执行完毕后,再执行下一个任务
2.4 各种队列的执行效果
- 使用
sync
函数往当前串行
队列中添加任务,会卡住当前的串行队列(产生死锁)
3. 死锁
3.1 死锁示例
// 问题:以下代码是在主线程执行的,会不会产生死锁?
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
3.2 死锁分析
如上代码
- 正常情况应该是顺序执行
任务1
、任务2
、任务3
任务2
使用dispatch_sync
同步执行方式,放入主线程队列
,因此任务2
需要排队等待前面的任务执行完成后才执行- 但是当前方法体
viewDidLoad
可以认为就是一个任务在执行,但是执行到任务2
的dispatch_sync
处,会等待dispatch_sync
执行完成再继续往下执行 - 此时,相当于
任务2
等待当前执行任务
执行完成,当前执行任务
也在等待任务2
执行完成,相互等待
因此造成线程死锁
3.3 其他示例
3.3.1 interview02
- (void)interview02 {
// 问题:interview01中的sync,改成 async。会不会产生死锁?不会!
/* 打印日志:
执行任务1
执行任务3
执行任务2
*/
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
3.3.2 interview03
- (void)interview03 {
// 会不会产生死锁?会!
/*
分析:
执行任务2 后,同步等待任务2 执行,但是因为queue是串行的,所以会相互等待
*/
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
3.3.3 interview04
- (void)interview04 {
// interview03改为并发队列(DISPATCH_QUEUE_CONCURRENT),会不会死锁?不会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
4. 案例
4.1 案例1
如下代码打印什么:
- (void)test {
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:0];
NSLog(@"3");
});
}
打印结果:
观察到,2
不会打印
去掉dispatch_async
又会怎么样
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:0];
NSLog(@"3");
}
这时候都有打印,只不过打印顺序1
>3
>2
接着,回到dispatch_async
里执行,但是把afterDelay
去掉
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil];
NSLog(@"3");
});
}
打印结果是1
>2
>3
,这次打印顺序是正常的
4.2 分析
上面的例子中,主要是考察runloop
与多线程
的相关知识
- 首先,在
dispatch_async
中使用- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
方法来执行,使用afterDelay:0
看似是在没有延迟的情况下执行,实际上因为该方法是基于runloop
的,相当于往runloop
添加一个定时器,但是因为此时我们是在子线程
中执行的,子线程
中的runloop
默认不会开启,所以test
方法没有执行。我们尝试开启runloop
dispatch_async(queue, ^{
NSLog(@"1");
[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]];
});
可以看到,2
打印了
-
接着,我们去掉了
dispatch_async
,发现打印结果是1
>3
>2
,为什么3
会比2
先打印的?
使用afterDelay:0
看似是在没有延迟的情况下执行,实际上因为该方法是基于runloop
的定时器,虽然没有延迟设置为 0,但是runloop
的定时器是在被唤醒
的时候处理定时器的,但是在进入休眠
之前会处理完点击事件
,因此看到的打印结果是1
、3
先打印,然后打印2
-
最后,回到
dispatch_async
中执行,只不过使用的是- (id)performSelector:(SEL)aSelector withObject:(id)object;
方法来执行,查看源码
该方法实际是直接使用objc_msgSend
方法执行,相当于我们直接[self test]
这样调用方法,所以这时候打印顺序是正常的1
>2
>3
5. 拓展
GNUstep
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
源码地址:https://gnustep.github.io/resources/downloads.html
虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值
@oubijiexi