本节学习内容目录:
问:iOS提供了哪些多线程技术方案?
pthread、NSThread、GCD、NSOperation
GCD
同步\异步 和 串行\并发
dispatch_barrier_async(barrier栅栏)
异步栅栏调用,可以很好的解决异步多读单写问题
dispatch_group
注意:GCD的队列执行是针对的并发,而不是并行
并发与并行是不一样的
并发与并行的区别
并发(Concurrence)是指:CPU在多个任务之间进行切换,同时执行好几个任务
并行(parallelism:平行)是指:两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
并行的事件或活动一定是并发的,但反之并发的事件或活动未必是并行的
同步\异步 和 串行\并发
sync:同步
async:异步
serial_queue:串行队列
concurrent_queue:并发队列
两两组合,可以由四种不同的组合:
同步串行
异步串行
同步并发
异步并发
同步串行
问:以下代码能执行吗?
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
[self test];
});
}
- (void)test
{
NSLog(@"test-[NSThread currentThread] - %@", [NSThread currentThread]);
}
结果:程序崩溃
为什么呢?
上段代码会产生死锁
那么,死锁产生的原因是什么呢?
可以概括为一句话:队列引起的循环等待
知识点:
主队列的任务要在主线程中执行
上图中涉及到了:
同步:sync
队列:主队列(串行)
线程:主线程
任务:viewDidLoad、Block
执行过程是:
主队列中有一个ViewDidLoad任务
通过
dispatch_sync(dispatch_get_main_queue(), ^{
[self test];
});
在主队列中添加新的任务Block
主队列是串行队列,因此按照队列先进先出的特性,先将主队列中的ViewDidLoad任务取出来放进主线程,执行任务。
由于ViewDidLoad任务中,同步调用了Block任务,会先将Block任务执行完毕后,继续执行ViewDidLoad任务。
换句话说,ViewDidLoad任务想要执行完,需要依赖Block任务执行完。也就是图中主队列左边的黑色箭头。
而Block任务想执行完,根据队列先进先出的特性,需要依赖ViewDidLoad任务执行完。也就是图中主队列右边的黑色箭头。
从而,两个任务形成相互等待,产生死锁。
问:异步分派到主队列,其任务执行在主线程还是子线程?
答:主线程
主队列的任务在主线程中执行
问:以下代码分别执行在哪个线程?
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1-[NSThread currentThread] - %@", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"2-[NSThread currentThread] - %@", [NSThread currentThread]);
});
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"3-[NSThread currentThread] - %@", [NSThread currentThread]);
});
});
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"4-[NSThread currentThread] - %@", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"5-[NSThread currentThread] - %@", [NSThread currentThread]);
});
});
}
运行结果:
4-[NSThread currentThread] - <NSThread: 0x600002355040>{number = 1, name = main}
1-[NSThread currentThread] - <NSThread: 0x6000023286c0>{number = 6, name = (null)}
5-[NSThread currentThread] - <NSThread: 0x600002355040>{number = 1, name = main}
2-[NSThread currentThread] - <NSThread: 0x600002355040>{number = 1, name = main}
3-[NSThread currentThread] - <NSThread: 0x600002355040>{number = 1, name = main}
1是在子线程,原因是异步并发执行任务
2是主线程,虽然2整体执行在线程6中,是子线程,且是async异步,具有开启子线程的能力,但是由于是主队列,所以任务执行在主线程
3是主线程,虽然3整体执行在线程6中,是子线程,且是sync同步,没有开启线程的能力,但是由于是主队列,所以任务执行在主线程
4是主线程,sync是同步,没有开启线程的能力,任务执行在当前线程,当前线程是主线程,因此任务执行在主线程
5是主线程,5整体执行在主线程,async是异步,有开启线程的能力,但是由于是主队列,所以任务执行在主线程
知识点:
Blocks submitted to the main queue MUST be run on the main thread
主队列的任务要在主线程中执行
同步异步主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力
创建队列的方式:(被问过)
//创建队列(方法一):串行队列
dispatch_queue_t queue = dispatch_queue_create(“myQueue”, NULL);//第一个参数是字符串,是队列的名称。第二个是队列属性,NULL代表串行
//创建队列(方法二):串行队列
dispatch_queue_t queue = dispatch_queue_create(“myQueue”, DISPATCH_QUEUE_SERIAL);
//创建队列(方法三):并发队列
dispatch_queue_t queue = dispatch_queue_create(“myQueue”, DISPATCH_QUEUE_CONCURRENT);
//创建队列(方法四):主队列(特殊的串行队列)
dispatch_queue_t queue = dispatch_get_main_queue();
//创建队列(方法五):全局队列(并发队列)
dispatch_queue_t queue = dispatch_get_global_queue(0, 0)
dispatch_queue_t dispatch_get_global_queue(long priority, unsigned long flags);
priority指定队列的优先级,一般为0或者DISPATCH_QUEUE_PRIORITY_DEFAULT
flag作为保留字段备用(一般为0)
问:下列代码执行有什么问题?
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL), ^{
NSLog(@"1-[NSThread currentThread] - %@", [NSThread currentThread]);
});
}
结果:
1-[NSThread currentThread] - <NSThread: 0x600001ead040>{number = 1, name = main}
正常执行,没有问题
dispatch_sync是同步执行,没有开启线程的能力,也就是Block任务是在主线程执行。
由于创建的队列是串行队列,没有产生由于队列产生的循环等待,因此,可以正常执行
问:以下代码执行结果是什么?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1-[NSThread currentThread] - %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2-[NSThread currentThread] - %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"3-[NSThread currentThread] - %@", [NSThread currentThread]);
});
NSLog(@"4-[NSThread currentThread] - %@", [NSThread currentThread]);
});
NSLog(@"5-[NSThread currentThread] - %@", [NSThread currentThread]);
}
打印结果:
1-[NSThread currentThread] - <NSThread: 0x6000014ead00>{number = 1, name = main}
2-[NSThread currentThread] - <NSThread: 0x6000014ead00>{number = 1, name = main}
3-[NSThread currentThread] - <NSThread: 0x6000014ead00>{number = 1, name = main}
4-[NSThread currentThread] - <NSThread: 0x6000014ead00>{number = 1, name = main}
5-[NSThread currentThread] - <NSThread: 0x6000014ead00>{number = 1, name = main}
结果分析:
1没问题
2,由于是同步,是立即在当前线程执行,当前线程是主线程,且是全局并发队列,因此,可以执行
其余结果一样。
问:以下代码执行结果是什么?
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1-[NSThread currentThread] - %@", [NSThread currentThread]);
[self performSelector:@selector(test) withObject:nil afterDelay:0.0];
NSLog(@"2-[NSThread currentThread] - %@", [NSThread currentThread]);
});
}
- (void)test
{
NSLog(@"test-[NSThread currentThread] - %@", [NSThread currentThread]);
}
打印结果:
1-[NSThread currentThread] - <NSThread: 0x600002baa780>{number = 3, name = (null)}
2-[NSThread currentThread] - <NSThread: 0x600002baa780>{number = 3, name = (null)}
可以发现,1和2的结果可以正常打印,而test不能够执行打印,为什么呢?
asyn异步,dispatch_get_global_queue(0, 0)全局并发,所以,block会在子线程执行,通过打印结果3也可以看出,确实是在子线程执行的任务。
Block会在GCD底层所维护的线程池当中的某一线程去执行,而GCD底层分派的线程,默认情况下是没有开启对应的RunLoop的。即使是0.0秒,也需要创建任务提交到RunLoop上面。由于RunLoop没有开启,因此,test方法不能够执行。
dispatch_barrier_async()
异步栅栏调用
问:如何利用GCD实现多读单写?
或者,如何实现一个多读单写?
使用dispatch_barrier_async()
dispatch_group_async()
问:如何实现这个需求:A、B、C三个任务并发,完成后执行任务D?
使用dispatch_group_async()
NSOperation
NSOperation是一个抽象类,如果需要使用的话,要用下面的子类:
NSInvocationOperation、NSBlockOperation、自定义子类
NSOperation的使用
上面两个的运行结果都是在当前线程中运行
上面的download方法,是在当前线程调用
而这三个下载图片操作都是在子线程中运行,且add Block只能是由NSBlockOperation创建出来的operation 才能调用的对象方法
上面的代码表面:只要加入队列中去,就是异步操作(开启子线程),且有队列的操作,不需要start操作
使用总结:
- 无论NSInvocationOperation还是NSBlockOperation,只要是单独创建,且只调用start方法,那么就是在当前线程(不具备开启新的线程的能力=同步)
- 由NSBlockOperation创建的operation,调用对象方法:addExecutionBlock,在子线程中运行
- 无论NSInvocationOperation还是NSBlockOperation,只要放在queue中,都是子线程(不需要调用start方法)
NSOperationQueue
队列分为主队列和非主队列,他们的创建方式分别如下:
主队列:[NSOperationQueue mainQueue]
非主队列:[[NSOperationQueue alloc] init]
NSOperation实现子线程完成后,统一操作
operation的所有操作完成之后,再调用此方法
operation.completionBlock= ^{}
一个现象
- (void)operationListen
{
//创建NSBlockOperation
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
for(int i = 0; i < 10; i++){
NSLog(@"-----下载图片1-----%@", [NSThread currentThread]);
}
}];
[operation addExecutionBlock:^{
for(int i = 0; i < 10; i++){
NSLog(@"-----下载图片2-----%@", [NSThread currentThread]);
}
}];
[operation start];
}
打印结果:
2024-01-19 15:36:18.711525+0800 OC_test_09[35507:1171176] -----下载图片1-----<_NSMainThread: 0x60000040c540>{number = 1, name = main}
2024-01-19 15:36:18.711533+0800 OC_test_09[35507:1171302] -----下载图片2-----<NSThread: 0x60000044f780>{number = 7, name = (null)}
…
下载图片1全部在主线程,下载图片2全部在子线程中完成
- (void)operationListen
{
//创建NSBlockOperation
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
for(int i = 0; i < 10; i++){
NSLog(@"-----下载图片1-----%@", [NSThread currentThread]);
}
}];
[operation addExecutionBlock:^{
for(int i = 0; i < 10; i++){
NSLog(@"-----下载图片2-----%@", [NSThread currentThread]);
}
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
//start不能和addOperation同时用,会报错:
//Thread 1: "*** -[NSBlockOperation start]: something is trying to start the receiver simultaneously from more than one thread"
//[operation start];
}
打印结果:
2024-01-19 15:40:06.302320+0800 OC_test_09[35724:1178860] -----下载图片2-----<NSThread: 0x6000009a48c0>{number = 4, name = (null)}
2024-01-19 15:40:06.302349+0800 OC_test_09[35724:1178861] -----下载图片1-----<NSThread: 0x6000009a5600>{number = 3, name = (null)}
…
上面的情况,下载图片1和2全都在子线程上运行,也就是说,只有有队列参与了,都是子线程
下载图片2肯定是子线程,没想到下载图片1也在子线程
总结:
NSBlockOperation的addExecutionBlock操作肯定是子线程
然而,他自己的block操作,如果是调用[operation start]方法,是当前线程。
如果是放在queue中的,是子线程
只要是放在queue中的都是子线程
并且,不能同时把operation放在队列queue中,又调用start方法。会奔溃的
问:NSOperation相比其他多线程技术方案有哪些优势和特点?
1 添加任务依赖
2 任务执行状态的控制
3 最大并发量的控制
任务执行状态的控制
问:可以控制NSOperation哪些状态?或者,关于NSOperation的任务状态都有哪些?
(需加强学习)
isReady
isExecuting
isFinished
isCancelled
NSThread(需加强)
问:如何通过NSThread结合RunLoop实现一个常驻线程?
问:NSThread的内部实现机制?
可通过GunStep查看和分享源码
内部创建了一个pthread线程
多线程和锁
iOS 多线程,自旋锁和互斥锁详解这个写的不错
问:iOS当中都有哪些锁?
或者,你在项目中,都使用过哪些锁?
这个问题第一种问法,你可以说你知道的,还好回答
第二种问法,就有点坑了,基本上都没用过。。。以后被问到用过哪些锁等同于介绍你知道的哪些锁
锁,大致分两种:
- 自旋锁
- 互斥锁
自旋锁是一种“忙等”的锁
所谓忙等,是指:如果当前锁已被其他线程获取,那么,当前线程会不断的探测该锁是否已经被释放。如果释放,则第一时间获取该锁🔐
自旋锁使用于轻量访问
自旋锁包括:
atomic
OSSpinLock:Spin自旋
互斥锁包括:
dispatch_semaphore_t:semaphore:信号量
pthread_mutex
NSLock
NSConditionLock
NSRecursiveLock:(Recursive:循环)
@synchronized:同步的
参考这个一起看
@synchronized同步
一般在创建单例对象的时候使用
注意两者:
@synchronized同步
@synthesize合成
synthesize是给实例变量起个别名
更多学习关于@synthesize
@synthesize的作用
atomic
修饰属性的关键字
对被修饰对象进行原子操作(不负责使用)
赋值 != 使用
OSSpinLock自旋锁
循环等待询问,不释放当前资源
用于轻量级数据访问,简单的int值+1,-1操作
例如:sidetable表中,有spinLock的使用
NSLock
问:下面代码会产生什么问题?为什么?
对同一把锁,重复加锁
解决方法:NSRecursiveLock
NSRecursiveLock
dispatch_semaphore_t信号量
Decrement the counting semaphore. If the resulting value is less than zero, this function waits for a signal to occur before returning.
计数信号量递减。如果结果值小于零,则该函数在返回之前等待出现信号。
Increment the counting semaphore. If the previous value was less than zero, this function wakes a waiting thread before returning.
增加计数信号量。如果前一个值小于零,该函数将在返回前唤醒一个等待的线程。
这个翻译,明明是 less than zero,小于0,怎么老是讲课是 <= 0?
仔细看,英文是the previous value
而,上一个英文是the resulting value
我理解的是:the resulting value 是结果值,也就是value,the previous value 是指的value前的值,由于value做了 value = value + 1;操作,也就是the resulting value = the previous value + 1;
the previous value < 0;
the resulting value - 1 < 0;
the resulting value < 1;
the resulting value <= 0;
有哥们找到了源码:
value > 0,则return 0
value <= 0则唤醒等待的线程。
信号量执行过程
假如有5个任务并发执行,信号量值value = 2;
dispatch_semaphore_wait信号量值value-1(2-1 = 1),结果1 >=0则进入执行任务1
dispatch_semaphore_wait信号量值value-1(1-1 = 0),结果0 = 0则进入执行任务2
dispatch_semaphore_wait信号量值value-1(0-1 = -1),结果-1 < 0,则主动阻塞等待任务3
dispatch_semaphore_wait信号量值value-1(-1-1 = -2),结果-2 < 0,则主动阻塞等待任务4
dispatch_semaphore_wait信号量值value-1(-2-1 = -3),结果-3 < 0,则主动阻塞等待任务5
任务1执行完毕
dispatch_semaphore_signal信号量值value + 1(-3 + 1 = -2),结果-2 < 0,则唤醒一个等待的任务3
任务2执行完毕
任务3执行完毕
dispatch_semaphore_signal信号量值value + 1(-2 + 1 = -1),结果-1 < 0,则唤醒一个等待的任务4
dispatch_semaphore_signal信号量值value + 1(-1 + 1 = 0),结果0 = 0,则唤醒一个等待的任务5
任务4执行完毕
任务5执行完毕
dispatch_semaphore_signal信号量值value + 1(0 + 1 = 1),结果1 > 0,return 0;
dispatch_semaphore_signal信号量值value + 1(1 + 1 = 2),结果2 > 0,return 0;
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
- (void)viewDidLoad {
[super viewDidLoad];
self.semaphore = dispatch_semaphore_create(2);
for (int i = 0; i<5; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
- (void)test
{
NSLog(@"dispatch_semaphore_wait");
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"dispatch_semaphore_signal");
dispatch_semaphore_signal(self.semaphore);
}
执行结果:
2020-07-31 19:18:58.821671+0800 block2[37362:733466] dispatch_semaphore_wait
2020-07-31 19:18:58.821472+0800 block2[37362:733464] dispatch_semaphore_wait
2020-07-31 19:18:58.821586+0800 block2[37362:733465] dispatch_semaphore_wait
2020-07-31 19:18:58.822075+0800 block2[37362:733467] dispatch_semaphore_wait
2020-07-31 19:18:58.822167+0800 block2[37362:733468] dispatch_semaphore_wait
2020-07-31 19:19:00.826471+0800 block2[37362:733466] dispatch_semaphore_signal
2020-07-31 19:19:00.845063+0800 block2[37362:733464] dispatch_semaphore_signal
2020-07-31 19:19:02.826900+0800 block2[37362:733465] dispatch_semaphore_signal
2020-07-31 19:19:02.848695+0800 block2[37362:733467] dispatch_semaphore_signal
2020-07-31 19:19:04.827253+0800 block2[37362:733468] dispatch_semaphore_signal
从结果时间来看,dispatch_semaphore_wait是一次性执行
dispatch_semaphore_signal是两秒执行两秒执行
由于进口处是2,则,最大并发数为2
dispatch_semaphore_wait若为负数,则执行_dispatch_semaphore_wait_slow进入等待。
dispatch_semaphore_signal能够唤醒一个在dispatch_semaphore_wait中等待的线程
参考文章:
iOS源码解析: GCD的信号量semaphore