前言
iOS的多线程有四种 : pthread , GCD , NSTread , NSOperation
, 前两种C语言编写 , 后两种OC语言编写 , NSThread
是对pthread
的封装 , NSOperation
是对GCD
的封装.
日常工作中我们最常用的是GCD
, GCD
会自动分配CPU资源
, 会自动管理线程的生命周期
, 使用起来非常方便 , 本文只讲述GCD
一 . 首先我们要明确几个概念:
1. 进程和线程
(1). 进程 : 在系统中
独立运行
并进行资源分配的基本单位 , 一个App就是一个进程-
(2). 线程 :
执行任务
的基本单元 , 一个App可以有多个线程线程创建好后 , 会放入可调度线程池中 , 等待CPU调度
线程的几种状态 : 新建 , 就绪 , 运行 , 死亡 , 阻塞
注意 : 一个CPU核心同一时间内只能执行一条线程 , 即单核CPU同一时间只能处理一条线程 , 多核CPU同一时间可以处理多条线程
-
(3). 多线程的作用,我们为什么要学习多线程?
提高程序执行效率 , 更好的利用多核CPU资源 , 多条线程可以同时利用多个CPU核心
防止阻塞主线程
在
GCD
中 , 线程执行完毕后会自动销毁这个线程
-
(4). 多线程的缺点 , 我们使用多线程时要注意的地方
线程太多会占用大量内存 , iOS系统中, 子线程一个线程会占用512KB的空间 , 主线程会占用1M的空间 , 开辟一个线程大约消耗90毫秒
线程太多会导致CPU调度线程开销过大 , 降低程序运行效率 (例如 : 单核CPU执行并发任务时 , CPU会对线程执行一个时间片规定的时间 , 然后快速切换到下一个线程, 由于时间片非常短 , 所以看起来是同时执行的 , 但是实际上是伪同时 , 线程太多会让CPU疲于调度 . CPU切换线程时 , 线程会在就绪状态和运行状态之间切换)
可能会造成死锁 (可以理解为互相等待)
数据竞争 (多个线程操作同一个数据,导致数据紊乱)
上述缺点的解决办法 , 我们会在下文中讲到
2. 队列和执行方式
-
(1). 两个常用概念
队列 : 存放任务的队形结构 , 系统以先进先出的方式(FIFO)调度队列中的任务
任务 : 任务就是在线程中要执行的代码
GCD的核心
: 将任务放到队列中 , 并指定执行方式(同步还是异步) , CPU会自动从队列中取出任务放到对应的线程中执行
-
(2). 两种通用队列
串行队列 : 在一条线程中 , 按顺序执行任务的队列
并发队列 : 可以在多个线程中 , 同时执行任务的队列 (其实会根据flag进行分组 , 高flag组的任务先同时执行 , 低flag组等待高flag组的任务执行完毕后 , 低flag组的任务才会同时执行)
不管是串行队列还是并发队列 , 都是按照
FIFO(先进先出)
的原则, 依次将任务分配线程**串行队列原理**
: CPU会从串行队列中取出一个任务 , 然后分配一个线程去执行 , 然后等待任务执行完毕后 , CPU继续取出任务 , 分配线程执行,也就是CPU会等待任务完成并发队列原理
: CPU会从并行队列中取出一个任务 , 然后分配一个线程去执行 , 不会等待任务执行完毕 , 就会CPU就会又去取出任务 , 然后分配新的线程去执行,也就是CPU会等待发生
(3). 两种执行方式
同步执行(sync) : 在当前线程执行任务 , 不会开辟新的线程 , 会阻塞当前线程 , 直到此任务执行完毕
异步执行(async) : 开辟新线程执行任务
- 同步会
阻塞当前线程
,直到此任务执行完毕 ; 异步不会阻塞线程
(4). 两种特殊队列
主队列-main : 系统为我们创建好的一个串行队列 , 它用来
管理主线程的任务
全局队列-global: 系统为我们创建好的并发队列 , 不受线程阻断
barrier
的影响
(5). 四种组合方式
串行队列同步执行 : 会
阻塞当前线程
, 让当前线程优先执行我这个串行队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 .串行队列异步执行 :
开辟1个新线程
, 在这个新线程中顺序执行任务并发队列同步执行 : 会
阻塞当前线程
, 让当前线程优先执行我这个并发队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 .并发队列异步执行 :
开辟多个新线程
, 在不同线程中同时执行不同任务
(6). 在主线程中使用主队列同步执行任务, 会不会造成线程死锁呢, 如果会,那么原因是什么呢?
答案是:会死锁,因为这么操作,会把任务加主队列中, 同时同步执行会阻塞主线程, 让主线程优先执行自己的任务, 而主队列是串行队列, 只能顺序执行任务, 所以主线程执行完串行队列里的任务之前, 是不会执行此任务的, 但是你不完成此任务 , 就一直阻塞主线程 , 一直阻塞主线程就会一直完成不了任务, 所以就会造成死锁
- (void)testDeadLock{
NSLog(@"开始");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"线程死锁了!");
});
NSLog(@"结束");
}
2018-08-28 15:52:39.675761+0800 Test[4277:586175] 开始
只会打印出这句话,然后到此就崩溃了
3. 线程锁 , 保证线程安全 (防止多线程抢夺同一块资源,主要防止同时写入)
-
互斥锁 : 让线程之间互相排斥 , 保证同一时间只能有一个线程去访问代码 , 使用
@synchronized(object){代码块}
可以加上互斥锁互斥锁原理 : 每一个对象内部都有一个锁 , 上面的object参数就是任意一个对象 , 它的内部是有一把锁的 , 当有线程要进入synchronized到代码块中就会先检查object的锁是打开还是关闭状态(默认锁是开打状态:1) . 如果锁是打开状态 , 线程就会执行代码块并上锁(0) , 执行完毕后就会解锁(1) ; 如果锁是关闭状态 , 线程想要执行代码块时 , 就要先等待 , 直到锁打开之后才能进入
-
自旋锁 : 以死循环的方式等待锁定的代码执行完毕 , 使用atomic属性 , 就可以加上自旋锁
自旋锁原理 : 如果发现有其它线程正在锁定代码 , 线程就会以死循环的方式 , 一直等待锁定的代码执行完毕 , 自选所更适合执行不耗时的代码 , automic属性内部就实现了自旋锁
了解了两种锁之后,我们思考一个问题,我们开启两个线程同时去买票,怎么保证线程安全?
@property (nonatomic,assign) NSInteger totalCount; //默认为10张票
//多线程买票
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[weakSelf buy];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[weakSelf buy];
});
}
- (void)buyTicket{
while (YES) {
if (self.totalCount > 0) {
self.totalCount = self.totalCount - 1;
NSLog(@"剩余%ld张票",(long)self.totalCount);
}else{
NSLog(@"卖光了...");
break;
}
}
}
想要解决这个问题 , 只需要保证对totalCount属性的读取和写入操作是原子性的 (读取和写入操作必须在一起执行) , 就可以保证线程安全 , 即对读取操作和写入操作同时加上互斥锁就可以.
- (void)buyTicket{
while (YES) {
//互斥锁
@synchronized (self) {
if (self.totalCount > 0) {
self.totalCount = self.totalCount - 1;
NSLog(@"剩余%ld",(long)self.totalCount);
}else{
NSLog(@"卖光了...");
break;
}
}
}
}
这里要注意的是 : 单纯的把nonatomic改成atomic是不行的 , 因为atomic保证了写入操作的线程安全 , 并没有同时保证读取和写入操作在一起执行.
我们来复习一下属性修饰符
nonatomic
: 非原子属性,多个线程可以访问
atomic
: 原子属性,保证线程安全,单写多读,针对多线程设计,默认值,保证同一时间只有一个线程能够写入,但是同一时间多个线程都可以取值,注意虽然写入操作是线程安全的,但是并不能保证读取操作和写入操作在一起执行
使用属性修饰符要注意的地方
1. strong和retain的区别
: strong在ARC下使用,retain在MRC下使用 ; 均是强引用,都会使引用计数增加
2. strong和copy的区别
: 本质上是setter方法的不同,在setter方法内部,strong仅仅存储了地址,而copy创建了一块内存去复制内容,并把新内存的地址存储起来```
3. weak和assign的区别
: weak只能在ARC下使用,修饰对象 ; assign只能用来修饰基本数据类型,在ARC和MRC下都可以使用 ; weak和assign都不会使引用计数增加 ; weak修饰的对象,一旦被释放就会被置为nil,给nil发送任何消息都没问题 ; 但是assigin修饰的对象被销毁后会维持原样,内存被销毁了,但是指针仍然指向那块内存,所以就会造成野指针导致崩溃
4. __block和__weak的区别
: __block对象在block中可以被重新赋值,__weak不可以 ; __weak修饰可以防止循环引用,__block不行
5. block为什么要用copy修饰?
Block按存放位置不同可以分为三类:NSGlobalBlock(全局Block),NSStackBlock(栈Block) , NSMallocBlock(堆Block),对于保存在栈上的Block,它的作用域有限,超出作用域,block就会销毁掉,使用block时就会导致野指针 ; 所以我们需要对栈区的Block copy到 堆区,防止Block过早的销毁
关于strong和weak原理的解释就需要引入内存管理的知识,并且引入autoreleasepool和RunLoop,有时间的话我会详细介绍
关于__strong和__weak的实现原理,请研究clang和RunTime
关于Block中循环引用问题,很好解决,但是原理是怎样的呢?待深入研究
二 . GCD的具体使用方法
1.GCD的基本使用
- (void)commonGCD{
dispatch_queue_t serialQueue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
#pragma - 串行队列同步执行
dispatch_sync(serialQueue, ^{
sleep(1);
NSLog(@"A---串行队列同步执行 : 会阻塞当前线程,让当前线程优先执行自己队列里的任务,当前线程 = %@",[NSThread currentThread]);
});
NSLog(@"A---串行队列同步执行 : 然后输出这句话,当前线程 = %@",[NSThread currentThread]);
#pragma - 串行队列异步执行
dispatch_async(serialQueue, ^{
sleep(1);
NSLog(@"B---串行队列异步执行 : 不会阻塞当前线程,会开辟一条新线程去执行自己队列里的任务,当前线程 = %@",[NSThread currentThread]);
});
NSLog(@"B---串行队列异步执行 : 和队列里的任务同时处理,当前线程 = %@",[NSThread currentThread]);
dispatch_queue_t concurrentQueue = dispatch_queue_create("并行队列", DISPATCH_QUEUE_CONCURRENT);
#pragma - 并发行队列同步执行
dispatch_sync(concurrentQueue, ^{
sleep(1);
NSLog(@"C---并发队列同步执行 : 会阻塞当前线程,让当前线程优先执行并发队列里的任务,当前线程 = %@",[NSThread currentThread]);
});
NSLog(@"C---并发队列同步执行 : 并发队列里的任务执行完毕才会输出,当前线程 = %@",[NSThread currentThread]);
#pragma - 并发行队列异步执行
dispatch_async(concurrentQueue, ^{
sleep(1);
NSLog(@"D1---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
sleep(1);
NSLog(@"D2---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = %@",[NSThread currentThread]);
});
NSLog(@"D---并发队列异步执行 : 不会阻塞当前线程,同时执行,当前线程 = %@",[NSThread currentThread]);
}
2018-08-27 17:37:28.164046+0800 Test[4601:829031] A---串行队列同步执行 : 会阻塞当前线程,让当前线程优先执行自己队列里的任务,当前线程 = {number = 1, name = main}
2018-08-27 17:37:28.164264+0800 Test[4601:829031] A---串行队列同步执行 : 然后输出这句话,当前线程 = {number = 1, name = main}
2018-08-27 17:37:28.164478+0800 Test[4601:829031] B---串行队列异步执行 : 和队列里的任务同时处理,当前线程 = {number = 1, name = main}
2018-08-27 17:37:29.165190+0800 Test[4601:829031] C---并发队列同步执行 : 会阻塞当前线程,让当前线程优先执行并发队列里的任务,当前线程 = {number = 1, name = main}
2018-08-27 17:37:29.165389+0800 Test[4601:829031] C---并发队列同步执行 : 并发队列里的任务执行完毕才会输出,当前线程 = {number = 1, name = main}
2018-08-27 17:37:29.165548+0800 Test[4601:829031] D---并发队列异步执行 : 不会阻塞当前线程,同时执行,当前线程 = {number = 1, name = main}
2018-08-27 17:37:29.166267+0800 Test[4601:829098] B---串行队列异步执行 : 不会阻塞当前线程,会开辟一条新线程去执行自己队列里的任务,当前线程 = {number = 3, name = (null)}
2018-08-27 17:37:30.165904+0800 Test[4601:829096] D2---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = {number = 5, name = (null)}
2018-08-27 17:37:30.165904+0800 Test[4601:829100] D1---并发队列异步执行 : 不会阻塞当前线程,会开辟多条线程执行自己的任务,当前线程 = {number = 4, name = (null)}
总结 :
-
串行
队列同步
执行 : 会阻塞当前线程 , 让当前线程优先执行我这个串行队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 . -
串行
队列异步
执行 : 开辟1个新线程 , 在这个新线程中顺序执行任务 -
并发
队列同步
执行 : 会阻塞当前线程 , 让当前线程优先执行我这个并发队列中的任务 , 按顺序执行完任务后 , 再去执行你原来的队列中的任务 . -
并发
队列异步
执行 : 开辟多个新线程 , 在不同线程中同时执行不同任务
上面说到 : 异步执行
会开辟新线程
,这里有一个特殊情况 :
主队列是一个串行队列 , 在主队列中异步执行任务 , 不会开辟新的线程 , 因为主队列中的任务只能由主线程来执行 , 所以当GCD要开辟线程去执行主队列中的任务时 , 发现是主队列 , 所以不予开辟新线程 , 主线程按顺序执行主队列的任务.
- (void)mainThreadGCD{
dispatch_queue_t mainQueue = dispatch_get_main_queue();
NSLog(@"主队列中异步执行任务-开始,当前线程 = %@",[NSThread currentThread]);
dispatch_async(mainQueue, ^{
NSLog(@"主队列中异步执行任务-正在执行,当前线程 = %@",[NSThread currentThread]);
});
NSLog(@"主队列中异步执行任务-结束,当前线程 = %@",[NSThread currentThread]);
sleep(1);
NSLog(@"主队列中异步执行任务-休眠1s后,当前线程 = %@",[NSThread currentThread]);
}
2018-08-27 17:28:03.478270+0800 Test[4517:813940] 主队列中异步执行任务-开始,当前线程 = {number = 1, name = main}
2018-08-27 17:28:03.478496+0800 Test[4517:813940] 主队列中异步执行任务-结束,当前线程 = {number = 1, name = main}
2018-08-27 17:28:04.478929+0800 Test[4517:813940] 主队列中异步执行任务-休眠1s结束,当前线程 = {number = 1, name = main}
2018-08-27 17:28:04.498234+0800 Test[4517:813940] 主队列中异步执行任务-正在执行,当前线程 = {number = 1, name = main}
2.GCD常见操作
(1). 延迟执行 , 如果队列是主队列 , 则在主线程中执行 ; 如果是全局队列或者自定义队列 , 则任务在子线程中执行
- (void)afterGCD{
dispatch_time_t afterTime = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC);
NSLog(@"开始");
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_queue_t customSerialQueue = dispatch_queue_create("自定义串行队列", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t customConcurrentQueue = dispatch_queue_create("自定义并发队列", DISPATCH_QUEUE_CONCURRENT);
dispatch_after(afterTime, mainQueue, ^{
NSLog(@"主队列---5s之后,当前线程 = %@",[NSThread currentThread]);
});
dispatch_after(afterTime, globalQueue, ^{
NSLog(@"全局队列---5s之后,当前线程 = %@",[NSThread currentThread]);
});
dispatch_after(afterTime, customSerialQueue, ^{
NSLog(@"自定义串行队列---5s之后,当前线程 = %@",[NSThread currentThread]);
});
dispatch_after(afterTime, customConcurrentQueue, ^{
NSLog(@"自定义并发队列---5s之后,当前线程 = %@",[NSThread currentThread]);
});
NSLog(@"结束");
}
2018-08-27 17:57:13.323480+0800 Test[4830:862430] 开始
2018-08-27 17:57:13.323710+0800 Test[4830:862430] 结束
2018-08-27 17:57:18.323898+0800 Test[4830:862576] 自定义并发队列---5s之后,当前线程 = {number = 5, name = (null)}
2018-08-27 17:57:18.323898+0800 Test[4830:862430] 主队列---5s之后,当前线程 = {number = 1, name = main}
2018-08-27 17:57:18.323898+0800 Test[4830:862575] 自定义串行队列---5s之后,当前线程 = {number = 3, name = (null)}
2018-08-27 17:57:18.323898+0800 Test[4830:862573] 全局队列---5s之后,当前线程 = {number = 4, name = (null)}
(2). 执行一次
- (void)onceGCD{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"在整个应用的生命周期内,只会执行一次,当前线程 = %@",[NSThread currentThread]);
});
}
2018-08-27 18:04:11.097953+0800 Test[4897:874867] 在整个应用的声明周期内,只会执行一次,当前线程 = {number = 1, name = main}
(3). 重复执行 , "执行完毕"一定会输出在最后的位置 , 因为dispatch_apply方法会阻塞当前线程
- (void)applyGCD{
dispatch_queue_t serialQueue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t concurrentQueue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"串行队列-重复执行5次是按顺序执行的");
dispatch_apply(5, serialQueue, ^(size_t count) {
sleep(1);
NSLog(@"第%zu此-当前线程=%@",count,[NSThread currentThread]);
});
NSLog(@"串行队列-重复执行完毕");
sleep(1);
NSLog(@"并发队列-重复执行5次都是同时执行的");
dispatch_apply(5, concurrentQueue, ^(size_t count) {
sleep(1);
NSLog(@"第%zu此-当前线程=%@",count,[NSThread currentThread]);
});
NSLog(@"并发队列-重复执行完毕");
}
2018-08-27 18:13:59.850946+0800 Test[5072:894227] 串行队列-重复执行5次是按顺序执行的
2018-08-27 18:14:00.851306+0800 Test[5072:894227] 第0此-当前线程={number = 1, name = main}
2018-08-27 18:14:01.851617+0800 Test[5072:894227] 第1此-当前线程={number = 1, name = main}
2018-08-27 18:14:02.853056+0800 Test[5072:894227] 第2此-当前线程={number = 1, name = main}
2018-08-27 18:14:03.853922+0800 Test[5072:894227] 第3此-当前线程={number = 1, name = main}
2018-08-27 18:14:04.855369+0800 Test[5072:894227] 第4此-当前线程={number = 1, name = main}
2018-08-27 18:14:04.855578+0800 Test[5072:894227] 串行队列-重复执行完毕
2018-08-27 18:14:05.856076+0800 Test[5072:894227] 并发队列-重复执行5次都是同时执行的
2018-08-27 18:14:06.857647+0800 Test[5072:894227] 第0此-当前线程={number = 1, name = main}
2018-08-27 18:14:06.857685+0800 Test[5072:894347] 第1此-当前线程={number = 4, name = (null)}
2018-08-27 18:14:06.857689+0800 Test[5072:894510] 第2此-当前线程={number = 3, name = (null)}
2018-08-27 18:14:06.857730+0800 Test[5072:894509] 第3此-当前线程={number = 5, name = (null)}
2018-08-27 18:14:07.860394+0800 Test[5072:894347] 第4此-当前线程={number = 4, name = (null)}
2018-08-27 18:14:07.860623+0800 Test[5072:894227] 并发队列-重复执行完毕
3.GCD的栅栏
-
barrier
将任务分割开执行 -
dispatch_barrier_async
异步分割 , 不会阻塞当前线程 -
dispatch_barrier_sync
同步分割 , 会阻塞当前线程
例如 : 你想并发异步执行任务 , 并且想先同时执行第1组和第2组任务,然后在同时执行第3组和第4组任务 , 就可以使用栅栏
- (void)barrierGCD{
dispatch_queue_t concurrentQueue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"第1组任务-栅栏-并发异步-%d-%@",i,[NSThread currentThread]);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"第2组任务-栅栏-并发异步2-%d-%@",i,[NSThread currentThread]);
}
});
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"---分割---");
NSLog(@"第1组和第2组任务是同时执行的");
NSLog(@"第3组和第4组任务也是同时执行的");
NSLog(@"但是第3组和第4组任务一定在第1组和第2组任务完成之后执行");
NSLog(@"---分割---");
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"第3组任务-栅栏-并发异步-%d-%@",i,[NSThread currentThread]);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"第4组任务-栅栏-并发异步2-%d-%@",i,[NSThread currentThread]);
}
});
NSLog(@"任务完成");
}
2018-08-27 18:30:01.262625+0800 Test[5240:921467] 第1组任务-栅栏-并发异步-0-{number = 3, name = (null)}
2018-08-27 18:30:01.262625+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-0-{number = 4, name = (null)}
2018-08-27 18:30:01.262648+0800 Test[5240:921366] 任务完成
2018-08-27 18:30:01.262927+0800 Test[5240:921467] 第1组任务-栅栏-并发异步-1-{number = 3, name = (null)}
2018-08-27 18:30:01.262959+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-1-{number = 4, name = (null)}
2018-08-27 18:30:01.263139+0800 Test[5240:921467] 第1组任务-栅栏-并发异步-2-{number = 3, name = (null)}
2018-08-27 18:30:01.263166+0800 Test[5240:921468] 第2组任务-栅栏-并发异步2-2-{number = 4, name = (null)}
2018-08-27 18:30:01.263289+0800 Test[5240:921468] ---分割---
2018-08-27 18:30:01.263393+0800 Test[5240:921468] 第1组和第2组任务是同时执行的
2018-08-27 18:30:01.263492+0800 Test[5240:921468] 第3组和第4组任务也是同时执行的
2018-08-27 18:30:01.263603+0800 Test[5240:921468] 但是第3组和第4组任务一定在第1组和第2组任务完成之后执行
2018-08-27 18:30:01.263709+0800 Test[5240:921468] ---分割---
2018-08-27 18:30:01.269175+0800 Test[5240:921468] 第3组任务-栅栏-并发异步-0-{number = 4, name = (null)}
2018-08-27 18:30:01.269175+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-0-{number = 3, name = (null)}
2018-08-27 18:30:01.269335+0800 Test[5240:921468] 第3组任务-栅栏-并发异步-1-{number = 4, name = (null)}
2018-08-27 18:30:01.269366+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-1-{number = 3, name = (null)}
2018-08-27 18:30:01.269574+0800 Test[5240:921468] 第3组任务-栅栏-并发异步-2-{number = 4, name = (null)}
2018-08-27 18:30:01.269909+0800 Test[5240:921467] 第4组任务-栅栏-并发异步2-2-{number = 3, name = (null)}
4.GCD的队列组
- 当任务都完成时 ,
dispatch_group_notify
的任务才会执行 ,dispatch_group_notify
不会阻塞当前线程 -
dispatch_group_wait
会阻塞当前线程 , 直到group的任务完成之后 , 才会继续往下执行 -
dispatch_group_enter
每执行一次 , group中的未完成任务就会+1 , group中的任务未完成之前 ,dispatch_group_notify
会不阻塞线程的等待 ,dispatch_group_wait
会阻塞当前线程式的等待. -
dispatch_group_leave
每执行一次 , group中的未完成任务就会-1 , group中的完成之后 ,dispatch_group_notify
会执行 ,dispatch_group_wait
也会开始执行.
- (void)groupNotifyGCD{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
NSLog(@"任务开始");
dispatch_group_async(group, globalQueue, ^{
sleep(1);
NSLog(@"任务1完成");
});
dispatch_group_async(group, globalQueue, ^{
sleep(2);
NSLog(@"任务2完成");
});
#pragma 当任务完成时,可以选择在哪个队列执行任务
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//如果是主队列,则此处为主线程
//如果是其它队列,则此处为子线程
NSLog(@"任务已完成");
});
NSLog(@"任务结束");
}
2018-08-28 09:49:26.141459+0800 Test[1184:54263] 任务开始
2018-08-28 09:49:26.141670+0800 Test[1184:54263] 任务结束
2018-08-28 09:49:27.142816+0800 Test[1184:54733] 任务1完成
2018-08-28 09:49:28.144000+0800 Test[1184:54731] 任务2完成
2018-08-28 09:49:28.144230+0800 Test[1184:54263] 任务已完成
- (void)groupWaitGCD{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
NSLog(@"任务开始");
dispatch_group_async(group, globalQueue, ^{
sleep(1);
NSLog(@"任务1完成");
});
dispatch_group_async(group, globalQueue, ^{
sleep(2);
NSLog(@"任务2完成");
});
dispatch_group_notify(group, globalQueue, ^{
NSLog(@"任务已完成,%@",[NSThread currentThread]);;
});
#pragma 会阻塞当前线程,直到group中的任务都结束后,才会执行"任务结束"
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"任务结束");
}
2018-08-28 09:59:21.225301+0800 Test[1314:80098] 任务开始
2018-08-28 09:59:22.226823+0800 Test[1314:80208] 任务1完成
2018-08-28 09:59:23.226653+0800 Test[1314:80209] 任务2完成
2018-08-28 09:59:23.226884+0800 Test[1314:80098] 任务结束
2018-08-28 09:59:23.226995+0800 Test[1314:80209] 任务已完成,{number = 3, name = (null)}
-(void)groupEnterAndLeave{
NSLog(@"开始");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(globalQueue, ^{
sleep(1);
NSLog(@"任务1完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(globalQueue, ^{
sleep(2);
NSLog(@"任务2完成");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"任务完成");
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"结束");
}
2018-08-28 10:37:44.867311+0800 Test[1589:111949] 开始
2018-08-28 10:37:45.870664+0800 Test[1589:112036] 任务1完成
2018-08-28 10:37:46.870663+0800 Test[1589:112039] 任务2完成
2018-08-28 10:37:46.870949+0800 Test[1589:111949] 结束
2018-08-28 10:37:46.890723+0800 Test[1589:111949] 任务完成
5.信号量
作用
- 将异步变成同步 , 其实除了信号量
semaphore
之外 ,dispatch_group
和dispatch_barrier
都可以做到将异步变成同步 , 思考一下怎么做吧. - 为线程加锁
用法
-
dispatch_semaphore_create
创建信号量 , 如果信号量小于0 , 会返回nil -
dispatch_semaphore_wait
会做一个判断 :- 如果当前信号量为0 , 则会阻塞当前线程 , 直到信号量大于0
- 如果当前信号量大于0 , 则会让当前信号量减一 , 并继续执行 (不会阻塞当前线程)
-
dispatch_semaphore_signal
会让信号量加一 , 不会阻塞线程 , 会唤醒被dispatch_semaphore_wait
阻塞的线程
信号量的基本用法 :
- (void)semaphoreGCD{
NSLog(@"开始");
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);
NSLog(@"任务完成");
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"结束");
}
2018-08-28 11:19:04.376679+0800 Test[1999:176567] 开始
2018-08-28 11:19:04.376917+0800 Test[1999:176567] 结束
2018-08-28 11:19:06.380869+0800 Test[1999:176609] 任务完成
异步变同步的三种方式 , 要注意防止主线程被阻塞导致的"卡死"
//异步变同步
- (void)asyncToSync{
#pragma mark - group
NSLog(@"任务1开始");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
sleep(1);
NSLog(@"任务1执行完毕");
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"任务1终于完成了,可以做其它的事情了");
#pragma mark - barrier
#pragma 自定义的并发队列,线程阻断才会生效
dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
#pragma 系统的并发队列,线程阻断无效
// dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
NSLog(@"任务2开始");
dispatch_async(queue, ^{
sleep(1);
NSLog(@"任务2执行完毕");
});
dispatch_barrier_sync(queue, ^{
NSLog(@"任务2完成了");
});
NSLog(@"任务2终于完成了,可以做其他事情了");
#pragma mark - semaphore
NSLog(@"任务3开始");
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"任务3执行完毕");
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"任务3终于完成了,可以做其他事情了");
}
2018-08-28 15:21:11.689522+0800 Test[3984:489019] 任务1开始
2018-08-28 15:21:12.690900+0800 Test[3984:489091] 任务1执行完毕
2018-08-28 15:21:12.691178+0800 Test[3984:489019] 任务1终于完成了,可以做其它的事情了
2018-08-28 15:21:12.691379+0800 Test[3984:489019] 任务2开始
2018-08-28 15:21:13.694621+0800 Test[3984:489091] 任务2执行完毕
2018-08-28 15:21:13.694876+0800 Test[3984:489019] 任务2完成了
2018-08-28 15:21:13.695004+0800 Test[3984:489019] 任务2终于完成了,可以做其他事情了
2018-08-28 15:21:13.695143+0800 Test[3984:489019] 任务3开始
2018-08-28 15:21:14.695499+0800 Test[3984:489091] 任务3执行完毕
2018-08-28 15:21:14.695726+0800 Test[3984:489019] 任务3终于完成了,可以做其他事情了
这里我们有疑问了 , 异步变同步有啥用? 那么我们看看AFNetworking里的源码就知道了 : (摘录自AFURLSessionManager.m)
#pragma mark -
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
继续回到上面的多线程买票的问题 , 利用semaphore
可以保证线程安全 , 思考下下面的代码里为什么要调用两次dispatch_semaphore_signal(_semaphore)
//注意 : 这里的_semaphore必须设置为全局变量 , 并且信号量初始值为1 , 才能保证线程安全 , 如果不设置为全局变量的话 , 多线程每次进来执行的时候都会新建一个信号量 , 没法保证线程安全了
- (void)semaphoreBuyTicket{
while (YES) {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (_totalCount > 0){
_totalCount = _totalCount - 1;
NSLog(@"还剩%ld",_totalCount);
}else{
NSLog(@"卖光了");
dispatch_semaphore_signal(_semaphore);
break;
}
dispatch_semaphore_signal(_semaphore);
}
}
练习题 : 以下代码会如何输出?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[weakSelf testSemaphoreGCD];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[weakSelf testSemaphoreGCD];
});
}
#pragma mark - 这里的_semaphore = dispatch_semaphore_create(1)
- (void)testSemaphoreGCD{
NSLog(@"开始111");
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"开始222");
sleep(5);
NSLog(@"结束");
dispatch_semaphore_signal(_semaphore);
}
答案 :
2018-08-28 12:03:51.868977+0800 Test[2527:242055] 开始111
2018-08-28 12:03:51.868974+0800 Test[2527:242059] 开始111
2018-08-28 12:03:51.869302+0800 Test[2527:242055] 开始222
2018-08-28 12:03:56.872708+0800 Test[2527:242055] 结束
2018-08-28 12:03:56.873042+0800 Test[2527:242059] 开始222
2018-08-28 12:04:01.875866+0800 Test[2527:242059] 结束