多线程学习(一)
开局几道面试题:
你理解的多线程?
iOS的多线程方案有哪几种?你更倾向于哪一种?
你在项目中用过GCD吗?
GCD的队列类型
说一下operationQueue和GCD的区别,以及各自的优势
线程安全的处理手段有哪些?
OC你了解的锁有哪些?
自旋锁和互斥锁对比
使用以上锁需要注意哪些?
用C/OC/C++,任选其一,实现自选或互斥
iOS中常见多线程方案
GCD
同步、异步
GCD中有2个用来执行任务的函数:同步、异步
用同步的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
其中,
queue:队列
block:任务
队列
GCD的队列可以分为2大类型:并发队列、串行队列
并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
并发功能只有在异步(dispatch_async)函数下才有效
串行队列(Serial Dispatch Queue)
让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
GCD带有creat的标识,不需要release
CF开头带有creat的标识,需要release
容易混淆的术语
同步、异步、并发、串行
同步异步主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力
并发串行主要影响:任务的执行方式 队列
并发:多个任务并发(同时)执行
串行:一个任务执行完毕后,再执行下一个任务
dispatch_sync和dispatch_async用来控制是否要开启新的线程
队列的类型,决定了任务的执行方式
主队列其实就是一个特殊的串行队列
dispatch_get_global_queue队列是一个全局并发队列
问:以下代码会执行吗?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
以上代码是有问题的
首先,我们知道queue是一个队列,既然是队列就要遵守FIFO规则。
而viewDidLoad函数的执行也是一个任务,该任务在主队列里面执行。
队列里面装的是 任务
dispatch_sync:立马在当前线程执行任务,执行完毕才能继续往下执行
dispatch_async:不需要立马执行完任务,不需要等待返回,就可以继续执行下面的任务
那么,
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
必须要执行完,才能执行后面的任务NSLog(@"执行任务3");
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
想要执行完,必须把里面的blockNSLog(@"执行任务2");执行完
而NSLog(@"执行任务2");是加在队列的最后面的,必须等队列中前面的任务执行完毕后,才能执行NSLog(@"执行任务2");。而队列中NSLog(@"执行任务2");的前一个任务是{内容},{内容} 想执行完毕的标志是NSLog(@"执行任务3");执行完毕。
也就是
dispatch_sync(queue, ^{ NSLog(@"执行任务2"); });等着NSLog(@"执行任务2");,NSLog(@"执行任务2");等着NSLog(@"执行任务3");,NSLog(@"执行任务3");等着dispatch_sync(queue, ^{ NSLog(@"执行任务2"); });
造成了循环等待
问:以下代码执行情况?
dispatch_async异步执行,不需要返回即可执行下面的代码,因此,不阻塞。
上面两个同样的代码,执行结果不一样,表明,任务2可以开启子线程。而任务2和任务3哪个先执行,不一定。
执行任务1被执行没问题。
由于第26行是一个dispatch_async异步,因此执行任务5打印也没问题。
由于25行创建的队列是一个串行队列,因此,任务是一个一个执行。
27行执行任务2打印没问题,并且开启了子线程。
28行是dispatch_sync同步,也就是必须等执行任务3执行完毕,才会执行任务4,而由于是串行队列,执行顺序是27-28-31执行完毕后才会执行29。造成循环等待,跟之前讲的在主线程阻塞是一个道理,只不过这次不是阻塞在主线程,而是阻塞在子线程。
总结
使用dispatch_sync往 当前 — 串行队列 中添加任务,会卡住当前的串行队列,从而产生死锁。
从上图中可以看出,dispatch_get_global_queue取出的队列是全局队列,并且是同一个。
dispatch_get_global_queue队列是一个全局并发队列
问:下面代码运行结果
结果:不会崩溃,但是不会开启100个线程
问:下面代码的运行结果是什么?为什么?
只有任务1和任务3被执行打印,而任务2没有执行。
使用[self performSelector:@selector(test) withObject:self];就可以顺序打印任务1-2-3。
如果将[self performSelector:@selector(test) withObject:self afterDelay:0.2];放在主线程,而不放在子线程,又会如何?
可以看出:在主线程可以正常打印,这是
为什么呢?
通过源码我们可以看到:
+ (id)performSelector:(SEL)sel withObject:(id)obj {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL, id))objc_msgSend)((id)self, sel, obj);
}
其就是一个objc_msgSend,也就是[self performSelector:@selector(test) withObject:self];
就是objc_msgSend(self, test)
。因此,可以打印。
可以看到- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;是在runloop文件下的。
也就是- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;有定时器功能,因此依赖于runloop下的定时器。然而,子线程默认runloop是没有打开的,需要自己手动去打开,因为没有打开runloop的执行,因此这句代码没有执行。即使时间是0.0,也是要加定时器,还是跟runloop有关。
因此,将runloop启动,即可执行定时器任务。
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];这段代码去掉,也会执行runloop,因此,runloop里面有timer,不为空,因此,可以执行。
面试题:
结果分析:
由于[thread start]一运行,36行代码就被执行了,运行完这个block,该thread就结束了。再执行39行代码,则target thread exited。Crash
结果分析:
由于加上了37-38两行代码,使得runloop被添加到线程里面,使得线程没有被销毁,因此可以执行任务2。当然,你把37行代码去掉,只保留38行代码也是可以达到同样的效果。
疑问:
为何3在最前面?
为何打印顺序是2-4,而不是4-2?
面试题
如何用GCD实现:
异步并发执行任务1、任务2
等任务1、任务2都执行完毕后,再回到主线程执行任务3
执行结果:
2020-06-12 11:18:27.812550+0800 GCD[43772:1429112] 执行任务2-<NSThread: 0x600000138f40>{number = 8, name = (null)}
2020-06-12 11:18:27.812607+0800 GCD[43772:1423846] 执行任务1-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.812778+0800 GCD[43772:1429112] 执行任务2-<NSThread: 0x600000138f40>{number = 8, name = (null)}
2020-06-12 11:18:27.813070+0800 GCD[43772:1423846] 执行任务1-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.813158+0800 GCD[43772:1423846] 执行任务1-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.813175+0800 GCD[43772:1429112] 执行任务2-<NSThread: 0x600000138f40>{number = 8, name = (null)}
2020-06-12 11:18:27.813358+0800 GCD[43772:1423846] 执行任务1-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.813491+0800 GCD[43772:1429112] 执行任务2-<NSThread: 0x600000138f40>{number = 8, name = (null)}
2020-06-12 11:18:27.813768+0800 GCD[43772:1429112] 执行任务2-<NSThread: 0x600000138f40>{number = 8, name = (null)}
2020-06-12 11:18:27.816792+0800 GCD[43772:1423846] 执行任务1-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.817313+0800 GCD[43772:1423846] 执行任务3-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.817402+0800 GCD[43772:1423846] 执行任务3-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.817503+0800 GCD[43772:1423846] 执行任务3-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.817584+0800 GCD[43772:1423846] 执行任务3-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
2020-06-12 11:18:27.817696+0800 GCD[43772:1423846] 执行任务3-<NSThread: 0x6000001334c0>{number = 3, name = (null)}
可以看到,任务1和任务2并发执行,任务3在任务1和任务2都执行完毕后再执行。
如果要回到主线程执行任务3,将最后的通知部分,换为:
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
for (int i = 0; i<5; i++) {
NSLog(@"执行任务3-%@", [NSThread currentThread]);
}
});
即可满足题目要求。
如果你想看一些苹果没有开源的代码是怎么执行的,你可以通过打断点,然后一步一步进,查看混编,看是如何执行的,这个难度*****五颗星。
为此,互联网人群通过一些列手段,制作了类似开源代码,也就是GUNstep。
GUNstep
GUNstep是GUN计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
GUNstep下载地址
虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值