本节主要理解:
1.GCD中任务和队列的理解
2.GCD中不同组合方式的注意点(同步串行/异步串行/同步并发/异步并发/同步 + 主队列(死锁)等)
3.线程间通讯
4.GCD的其他使用方法(栅栏函数/线程组/信号量/延时执行/只执行一次等)
多线程:在同一时刻,一个CPU只能处理1条线程,但CPU可以在多条线程之间快速的切换,只要切换的足够快,就造成了多线程一同执行的假象
本文主要用来总结多线程中一些常见的易错易忘点,主要侧重在GCD
为什么推荐使用GCD呢,具体如下:
- GCD 可用于多核的并行运算;
- GCD 会自动利用更多的 CPU 内核(比如双核、四核);
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
一.死锁
两个必要条件
1.sync (同步任务)
2.往当前的串行队列添加任务
一个典型的串行队列
dispatch_async(dispatch_get_main_queue(), ^{
});
例1
// 问题:以下代码是在主线程执行的,会不会产生死锁?会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
例2
// 问题:以下代码是在主线程执行的,会不会产生死锁?会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
例3
// 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
例4
// 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
NSLog(@"执行任务1");
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, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
二.子线程runloop的使用
子线程中runloop需要手动激活
NSTimer依附于runloop生效
一个典型的并发队列
全局并发队列
dispatch_get_global_queue(0,0)
调用test2方法如何打印(1-3-2)
- (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");
//手动激活子线程的runloop,执行子线程runloop中的定时器,如果不手动激活,则不会执行test方法
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
}
- (void)test
{
NSLog(@"2");
}
三.线程安全
解决方案:线程同步(依赖关系)与线程安全(数据读写)
常见线程同步方法:加锁
根据性能与使用方便性推荐使用 dispath_semaphore(信号量)与 pthread_mutex
//信号量的初始值
int value = 1;
//初始化信号量
dispatch_ semaphore_ .t semaphore = dispatch_ semaphore_ create(value) ;
//如果信号量的值<=0,当前线程就会进入休眠等待(直到信号量的值>0)
//如果信号量的值>0,就减1,然后往下执行后面的代码
dispatch_ semaphore_ wait ( semaphore, DISPATCH_ .TIME_ FOREVER);
//让信号量的值加1
dispatch_ semaphore_ _signal (semaphore);
例
/**
* 线程安全:使用 semaphore 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
semaphoreLock = dispatch_semaphore_create(1);
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
}
}
输出结果为:
2019-08-08 15:23:58.819891+0800 YSC-GCD-demo[18116:4348091] currentThread---<NSThread: 0x600000681380>{number = 1, name = main}
2019-08-08 15:23:58.820041+0800 YSC-GCD-demo[18116:4348091] semaphore---begin
2019-08-08 15:23:58.820305+0800 YSC-GCD-demo[18116:4348159] 剩余票数:49 窗口:<NSThread: 0x6000006ede80>{number = 3, name = (null)}
2019-08-08 15:23:59.022165+0800 YSC-GCD-demo[18116:4348157] 剩余票数:48 窗口:<NSThread: 0x6000006e4b40>{number = 4, name = (null)}
2019-08-08 15:23:59.225299+0800 YSC-GCD-demo[18116:4348159] 剩余票数:47 窗口:<NSThread: 0x6000006ede80>{number = 3, name = (null)}
...
2019-08-08 15:24:08.355977+0800 YSC-GCD-demo[18116:4348157] 剩余票数:2 窗口:<NSThread: 0x6000006e4b40>{number = 4, name = (null)}
2019-08-08 15:24:08.559201+0800 YSC-GCD-demo[18116:4348159] 剩余票数:1 窗口:<NSThread: 0x6000006ede80>{number = 3, name = (null)}
2019-08-08 15:24:08.759630+0800 YSC-GCD-demo[18116:4348157] 剩余票数:0 窗口:<NSThread: 0x6000006e4b40>{number = 4, name = (null)}
2019-08-08 15:24:08.965100+0800 YSC-GCD-demo[18116:4348159] 所有火车票均已售完
2019-08-08 15:24:08.965440+0800 YSC-GCD-demo[18116:4348157] 所有火车票均已售完
四.线程间通讯
以下三种方法通过线程的依赖关系实现线程同步:
1.组队列(dispatch_group_t)
2.阻塞任务(dispatch_barrier_(a)sync)
3.信号量机制(dispatch_semaphore)
4.子线程执行耗时操作,主队线程刷新UI ,使用dispatch_get_main_queue回到主线程
五.NSOperation与GCD的区别
1.GCD的核心是C语言写的系统服务,执行和操作简单高效,因此NSOperation底层也通过GCD实现,换个说法就是NSOperation是对GCD更高层次的抽象,这是他们之间最本质的区别.因此如果希望自定义任务,建议使用NSOperation;
2.依赖关系,NSOperation可以设置两个NSOperation之间的依赖,第二个任务依赖于第一个任务完成执行,GCD无法设置依赖关系,不过可以通过dispatch_barrier_async来实现这种效果;
3.KVO(键值对观察),NSOperation和容易判断Operation当前的状态(是否执行,是否取消),对此GCD无法通过KVO进行判断;
4.优先级,NSOperation可以设置自身的优先级,但是优先级高的不一定先执行,GCD只能设置队列的优先级,无法在执行的block设置优先级;
5.继承,NSOperation是一个抽象类实际开发中常用的两个类是NSInvocationOperation和NSBlockOperation,同样我们可以自定义NSOperation,GCD执行任务可以自由组装,没有继承那么高的代码复用度;
6.效率,直接使用GCD效率确实会更高效,NSOperation会多一点开销,但是通过NSOperation可以获得依赖,优先级,继承,键值对观察这些优势,相对于多的那么一点开销确实很划算,鱼和熊掌不可得兼,取舍在于开发者自己;
六.多线程常见面试题
1.你理解的多线程 ?
2.iOS的多线程方案有哪几种 ?你更倾向于哪一种?
3.你在项目中用过 GCD吗?
4.GCD 的队列类型
5.说一下 OperationQueue和GCD的区别,以及各自的优势
6.线程安全的处理手段有哪些?
7.OC你了解的锁有 哪些?在你回答基础上进行二次提问;
-追问一:自旋和互斥对比?
-追问二:使用以上锁需要注意哪些?
-追问三:用C/OC/C++ ,任选其一,实现自旋或互斥?口述即可!
参考:
iOS 多线程:『GCD』详尽总结
# iOS开发-NSOperation与GCD区别
关于iOS多线程,你看我就够了
多线程之GCD 线程间的通信
网友评论