章前回顾
上章我们了解了锁的一些知识,线程安全需要锁的协助。这章我们探索一下多线程原理篇;
初识
周知,了解多线程首先需要捋一下线程、进程、同步、异步、串行、并行、死锁等概念与关系。
多线程原理
线程:1、进程的基本执行单元,一个进程的所有任务必须需要在线程中执行;2、进程中至少需要一条线程;3、程序启动默认创建一条线程,创建的线程为主线程(UI线程);
进程:1、每个进程是独立的,拥有独立的内存空间;2、进程是指在系统正在运行的一个程序;
线程 | 进程 | 对比 | |
---|---|---|---|
地址空间 | 一个进程内线程地址共享 | 每个进程都有一个独立内存地址 | 各有优势 |
创建、销毁、切换 | 切换简单,速度快 | 切换复杂,速度慢 | 线程占优 |
可靠性 | 一个线程挂掉会导致整个进程挂 | 每个进程都是独立的,互不影响 | 进程健壮 |
效率 | 占用内存小,切换简单,CPU利用率高 | 占用内存空间较大,切换复杂,CPU利用率低 | 线程占优 |
分布 | 适用于多核分布式 | 适用多核、多机分布 | 进程占优 |
进程与线程的关系:
可以把iOS系统
看做一个商场
,进程
(APP)则是商场中的店铺
,线程
就是类似店铺雇佣的员工
。
进程之间相互独立(奶茶店、果汁店):
·奶茶店看不了果汁店的账目(访问不了别的进程的内存)
·果汁店用不了奶茶店的波霸 (进程间的资源是相互独立的)
进程至少要一条线程:
·店铺里至少需要一名员工(进程至少有一个线程)
·店铺早上开店的员工 (进程中的主线程)
进程/线程崩溃
·奶茶店倒闭了,并不影响果汁店营业(进程崩溃不会对其他进程有影响)
·奶茶店的收银员不干了导致店铺无法正常营业(线程崩溃进而影响进程瘫痪)
同步:在当前线程中执行任务,不具备创新新线程能力;
异步:在新线程中执行任务,能够开启新线程;
串行:一个任务执行完毕执行下个任务,按顺序执行;
并行:多个任务同时(并发)执行;
主队列:特殊的串行队列,在主线程执行;
同步函数:不会创建新线程,当前线程执行,执行完毕后才能继续往下执行任务,其中一条任务没执行完成,会卡住改函数,不会往下执行;
异步函数:创建新线程,不要求当前线程执行完成,会等上个任务执行完后再执行;
串行队列:任务按顺序执行,一个任务一个任务执行;
并行队列:多个任务同时执行,异步函数下有效,开启多个线程;
串行队列 | 并行队列 | 主队列 | |
---|---|---|---|
同步(sync) | ·没有开启新线程 ;·串行执行任务(卡住当前队列) | ·没有开启新线程 ;·串行执行任务(有序) | ·没有开启新线程 ;·串行执行任务(死锁) |
异步(async) | ·有开启新线程 ;·串行执行任务(无序) | ·有开启新线程 ;·并发执行任务(无序) | ·有开启新线程 ;·串行执行任务(无序) |
死锁:
具有队列的特点,FIFO;
产生死锁的情况以及解决方案:
- 主队列同步函数,会产生死锁;解决方案:主队列(mainQueue)异步,实现串行执行
- 串行队列使用同步函数,会产生死锁,即使用async函数往串行队列中添加任务;解决方案:需要按顺序执行:并行队列加入同步函数执行/串行队列异步执行;需要并发执行:并行队列中加入异步函数执行
栅栏函数:
特点:控制任务的执行顺序。(栅栏函数之前的执行完毕之后,执行栅栏函数,然后在执行栅栏函数之后的)
在并行队列实现异步函数里,顺序是不固定的,加入栅栏函数可以使其按顺序执行,或者在串行队列异步函数也可以实现控制执行顺序
dispatch_queue_t queue = dispatch_queue_create("com.xx.def", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (NSInteger i =0; i < 3; i++) {
NSLog(@"%ld TestLine1 %@",i,[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i =0; i < 3; i++) {
NSLog(@"%ld TestLine2 %@",i,[NSThread currentThread]);
}
});
dispatch_barrier_async(queue, ^{
NSLog(@"这是个栅栏函数");
});
dispatch_async(queue, ^{
for (NSInteger i =0; i < 3; i++) {
NSLog(@"%ld TestLine3 %@",i,[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i =0; i < 3; i++) {
NSLog(@"%ld TestLine4 %@",i,[NSThread currentThread]);
}
});
结果:
2020-04-05 00:20:27.690434+0800 Test-OC[31185:1550291] 0 TestLine2 <NSThread: 0x6000005e3a00>{number = 4, name = (null)}
2020-04-05 00:20:27.690482+0800 Test-OC[31185:1550290] 0 TestLine1 <NSThread: 0x6000005f2340>{number = 3, name = (null)}
2020-04-05 00:20:27.690693+0800 Test-OC[31185:1550290] 1 TestLine1 <NSThread: 0x6000005f2340>{number = 3, name = (null)}
2020-04-05 00:20:27.690696+0800 Test-OC[31185:1550291] 1 TestLine2 <NSThread: 0x6000005e3a00>{number = 4, name = (null)}
2020-04-05 00:20:27.690827+0800 Test-OC[31185:1550290] 2 TestLine1 <NSThread: 0x6000005f2340>{number = 3, name = (null)}
2020-04-05 00:20:27.690936+0800 Test-OC[31185:1550291] 2 TestLine2 <NSThread: 0x6000005e3a00>{number = 4, name = (null)}
2020-04-05 00:20:27.691053+0800 Test-OC[31185:1550291] 这是个栅栏函数
2020-04-05 00:20:27.691229+0800 Test-OC[31185:1550291] 0 TestLine3 <NSThread: 0x6000005e3a00>{number = 4, name = (null)}
2020-04-05 00:20:27.691950+0800 Test-OC[31185:1550290] 0 TestLine4 <NSThread: 0x6000005f2340>{number = 3, name = (null)}
2020-04-05 00:20:27.692283+0800 Test-OC[31185:1550291] 1 TestLine3 <NSThread: 0x6000005e3a00>{number = 4, name = (null)}
2020-04-05 00:20:27.692670+0800 Test-OC[31185:1550290] 1 TestLine4 <NSThread: 0x6000005f2340>{number = 3, name = (null)}
2020-04-05 00:20:27.692802+0800 Test-OC[31185:1550291] 2 TestLine3 <NSThread: 0x6000005e3a00>{number = 4, name = (null)}
2020-04-05 00:20:27.692989+0800 Test-OC[31185:1550290] 2 TestLine4 <NSThread: 0x6000005f2340>{number = 3, name = (null)}
dispatch_group_notify
应用类似场景:用户下载一个图片,图片很大,需要分成很多份进行下载,使用GCD应该如何实现?使用什么队列?下载完后统一刷新UI
使用Dispatch Group追加Block到Gobal Group Queue,最后使用group_notify监听执行完所有group block后执行notify 放入主线程的block;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{// 合并图片 });
多线程的实现(技术方案)
更多详细使用可以参考:多线程详解
-
pthread
优点:
· C语言,适用Unix/Linux/Windows等系统
·可跨平台,可移植性,轻量级
缺点:
·需要手动管理生命周期
·api较为复杂,适用难度较大
-
NSThread
优点:
· 使用面向对象化
·易用,可直接操作线程对象
缺点:
·需要手动管理生命周期
-
GCD
优点:
· 基于C语言,自动管理线程生命周期
·多核并行运算的解决方案
·使用简易,性能优异,经常使用
缺点:
·在并发队列中,顺序控制相对复杂。
-
NSOperation
优点:
· 基于GCD面向对象的OC封装,更加面向对象化,自动管理线程生命周期
·可以添加线程依赖,控制线程总数,最大并发数
·监听线程完成情况
·NSOPeration是一个抽象的基类,表示一个独立的运算单元。可以为子类提供更有效且线程安全的建立状态,优先级,线程总数,依赖关系和取消等操作
缺点:
·使用面向对象,创建使用起来相对GCD更加复杂些,在不考虑线程控制,依赖关系下优先选择GCD;
多线程的生命周期
线程的生命周期分为5部分:新建,准备就绪,运行(执行),阻塞,销毁
生命周期流程图:
- 新建:实例化新线程
- 准备就绪:向线程对象发送start消息,将线程对象加入线程池可调度状态,等待CUP调度
- 运行(执行):CUP在可调度线程池中执行线程任务;当前任务执行完毕,CUP会进入一个就绪状态,即进入短暂的等待期,在此期间如果有新任务需要执行,则执行新任务,否则该线程会销毁。等有下个任务执行,会开启新的线程执行。
-
阻塞:当满足某个预定条件时,可以使用锁或者休眠,来阻塞线程执行;sleepForTimeInterval(休眠指定时长)sleepUntilDate(休眠到指定日期),@synchronized(
object
)(互斥锁)。 - 销毁:正常销毁:线程执行完毕。非正常销毁:满足某个条件,线程内部终止执行或者主线程终止线程对象;
线程生命周期联系思维导图:
标注:图片等资源参考自多线程原理
线程池实现的原理
线程池工作原理流程图:
线程与RunLoop关系
- runLoop和线程是一一对应的关系,一个runLoop对应一个核心线程。核心:runLoop可以嵌套多个多个线程,但是核心只有一个。关系保存在全局字典中。
- 在主线程中,在程序启动时,runLoop就默认创建了。
- 在子线程中,runLoop是懒加载状态,只有使用时才会创建;子线程中使用定时器需注意:确保子线程runLoop的创建,不然定时器不会开启;
- runLoop是用来管理线程的,当线程的runLoop被开启,线程在执行完毕后会进入休眠状态,等待新任务执行才会被唤醒
- runLoop在主线程被开启时创建,线程结束时被销毁。
线程安全介绍看官们可以移位:iOS多线程安全-锁