iOS底层探索 -- 多线程
1. 多线程概念问题
-
线程的定义
- 线程是进程的基本单元,一个进程的所有任务都在线程中执行
- 进程要想执行任务,必须得有线程,进程至少有有一条线程
- 程序启动默认会开启一条线程,这条线程被称为主线程或者UI线程
-
进程的定义
- 进程是指在系统中正在运行的一个应用程序
- 每个进程之间是互相独立的,每个进程均运行在其专用的且受保护的内存中
-
进程和线程的关系
-
地址空间:同一进程的线程共享本进程的地址空间,而进程之间相互独立
-
资源拥有:同一进程的线程共享本进程的资源(如:内存、CPU),进程之间是相互独立的
-
一个进程崩溃会,在保护模式下不会对其他进程产生影响,但是一个线程崩溃,整个进程都死掉,所以,多进程要不多线程健壮(iOS没有多进程)
-
进程切换是,消耗的资源大,效率高,所以涉及频繁切换进程时,使用线程要优于进程。同样如果要求同时进行并且又要共享某些变量的并发造作,只能用线程
-
执行过程:每个独立的进程有一个程序运行的入口,顺序执行序列和程序入口,但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
-
线程是处理器调度的基本单元,进程不是
-
-
多线程的意义
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU,内存)
- 线程上的任务执行完成后,线程会自动销毁
- 开启线程需要占用一定的内存空间(默认 每个线程都占 512K )
- 如果开启大量的线程,会占用大量的内存
- 程序设计更加复杂,比如线程之间的通讯,多线程共享数据
-
线程和
Runloop
的关系runloop
与线程是一一对应的,一个runloop
对应一个核心的线程(runloop
是可以嵌套的,但是核心的线程只能有一个,他们的关系保存在一个字典里)runloop
是管理线程的,当线程的runloop
被开启后,线程会在执行完任务后进入休眠状态,有任务会被唤醒去执行任务runloop
在第一次获取是被常见,在线程结束是销毁- 对主线程来说,
runloop
在程序一启动就默认创建好 - 对于子线程来说,
runloop
是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意,确保子线程的runloop
被创建,不然定时器不会回调。
-
atomic
和nonatomic
的区别-
nonatomic
非原子性,非线程安全,适合内存小的移动设备 -
atomic
原子属性(线程安全,需要消耗大量的资源),针对多线程设计的,默认值保证同一时间只有一个线程能够写入(可以同一时间多个线程取值)
本身是一把自旋锁 -
建议使用
nonatomic
声明属性,尽量避免多线程抢夺同一资源 -
尽量将加锁,资源抢夺的业务逻辑交给服务器处理,减少客户端压力
-
-
C
和OC
的桥接__bridge
只做类型转换,不修改对象(内存)管理权__bridge_retained
将OC对象
转换为Core Foundation对象
,同时将对象(内存)的管理权交给我们,后续需要 使用CFRelease或者相关方法来释放对象__bridge_transfer
将Core Foundation
的对象 转换为Objective-C
的对象,同时将对象(内存)的管理权交给ARC
2. 多线程原理
-
多线程原理
CUP
在单位时间片里快速在各个线程之前切换- 多核是为了加快执行的效率
-
多线程生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e4PZRCdP-1588931804943)(https://user-gold-cdn.xitu.io/2020/5/8/171f23ec24188bc2?w=1167&h=394&f=png&s=70827)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FaQyPWJ4-1588931804964)(https://user-gold-cdn.xitu.io/2020/5/8/171f2472ba57bdeb?w=1137&h=518&f=png&s=82193)]
饱和策略:
AbortPolicy
直接抛出RejectedExecutionExeception
异常来阻止系统正常运行CallerRunsPolicy
将任务回退到调用者DisOldestPolicy
丢掉等待最久的任务DisCardPolicy
直接丢弃任务- 这四种拒绝策略均实现的
RejectedExecutionHandler
接口
3. 端口通讯
VC
类
//1. 创建主线程的port
// 子线程通过此端口发送消息给主线程
self.myPort = [NSMachPort port];
//2. 设置port的代理回调对象
self.myPort.delegate = self;
//3. 把port加入runloop,接收port消息
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
self.person = [[KCPerson alloc] init];
[NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
toTarget:self.person
withObject:self.myPort];
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"VC == %@",[NSThread currentThread]);
NSLog(@"从person 传过来一些信息:");
//会报错,没有这个隐藏属性
//NSLog(@"from == %@",[message valueForKey:@"from"]);
NSArray *messageArr = [message valueForKey:@"components"];
NSString *dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"传过来一些信息 :%@",dataStr);
NSPort *destinPort = [message valueForKey:@"remotePort"];
if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
NSLog(@"传过来的数据有误");
return;
}
NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];
// 非常重要,如果你想在Person的port接受信息,必须加入到当前主线程的runloop
[[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
NSLog(@"VC == %@",[NSThread currentThread]);
BOOL success = [destinPort sendBeforeDate:[NSDate date]
msgid:10010
components:array
from:self.myPort
reserved:0];
NSLog(@"%d",success);
}
Person
类
- (void)personLaunchThreadWithPort:(NSPort *)port{
NSLog(@"VC 响应了Person里面");
@autoreleasepool {
//1. 保存主线程传入的port
self.vcPort = port;
//2. 设置子线程名字
[[NSThread currentThread] setName:@"KCPersonThread"];
//3. 开启runloop
[[NSRunLoop currentRunLoop] run];
//4. 创建自己port
self.myPort = [NSMachPort port];
//5. 设置port的代理回调对象
self.myPort.delegate = self;
//6. 完成向主线程port发送消息
[self sendPortMessage];
}
}
/**
* 完成向主线程发送port消息
*/
- (void)sendPortMessage {
NSData *data1 = [@"Gavin" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
// 发送消息到VC的主线程
// 第一个参数:发送时间。
// msgid 消息标识。
// components,发送消息附带参数。
// reserved:为头部预留的字节数
[self.vcPort sendBeforeDate:[NSDate date]
msgid:10086
components:array
from:self.myPort
reserved:0];
}
#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"person:handlePortMessage == %@",[NSThread currentThread]);
NSLog(@"从VC 传过来一些信息:");
NSLog(@"components == %@",[message valueForKey:@"components"]);
NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
NSArray *messageArr = [message valueForKey:@"components"];
NSString *dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"传过来一些信息 :%@",dataStr);
}
解析:
1. 在 VC 中创建主线程的port(子线程通过此端口发消息给主线程),并设置 port 的代理回调对象
2. 将 port 加入到 runloop,接收 port 消息。
3. 实现 handlePortMessage 代理方法
4. GCD 初探
GCD
是一套纯 C 语言 API
,提供了非常多的强大的函数,自动管理线程的生命周期(创建线程,调度任务,销毁线程)。只需要将任务添加到队列,并且指定执行任务的函数
任务是由block
封装的(无参数,无返回值)
执行任务的函数有两种:
- 异步
dispatch_async
,不用等待当前语句执行完毕,就可以执行下一条语句,会开辟新的线程执行任务 - 同步
dispatch_sync
,必须等待当前语句执行完毕,才会执行下一条语句,不会开辟新的线程,在当前线程执行block
任务
队列分为两种:
- 串行队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fdzsliej-1588931804967)(https://user-gold-cdn.xitu.io/2020/5/8/171f325d8797889b?w=627&h=222&f=png&s=32586)]
- 并发队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-96uuEc6I-1588931804979)(https://user-gold-cdn.xitu.io/2020/5/8/171f3260da4cf0a3?w=742&h=291&f=png&s=38456)]
那么还有下面两种队列:主队列和全局队列
-
主队列
dispatch_get_main_queue()
- 专门用来在住线程上调度任务的队列,不会开启线程
- 如果当前住线程正在有任务执行,那么无论主队列中当前添加了什么任务都不会被调度
-
全局队列
dispatch_get_global_queue()
- 是一个并发队列
- 在使用多线程开发是,如果队列没有特殊需求,在执行异步任务时,可以直接使用全局队列
队列和函数组合有以下四种情况:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q6RsjtHz-1588931804981)(https://user-gold-cdn.xitu.io/2020/5/8/171f32c21bc01ac7?w=1030&h=513&f=png&s=76597)]
接下来,看一下
GCD
的经典面试题
- (void)textDemo{
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
// 1 5 2 4 3
}
上面代码打印结果是什么?
其实上面的代码很简单,打印结果是1 5 2 4 3
分析:
首先是打印 1 ,然后是并发队列加异步函数,这个操作是异步耗时,所以先执行打印 5,
然后是耗时操作,打印 2 ,接着又是并发队列加异步函数,同理先打印 4, 在打印3。
接着看下面的题目
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
/***
1 2 3
0
7 8 9
*/
dispatch_async(queue, ^{
// sleep(2);
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
// 堵塞 - 护犊子
dispatch_sync(queue, ^{
NSLog(@"3");
});
// **********************
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
// A: 1230789
// B: 1237890
// C: 3120798
// D: 2137890
上面的结果是A C
。
分析:
首先是并发队列,然后是两个异步耗时操作,紧接着是一个同步函数,这样会堵塞后面的任务
的执行,再接着是两个异步操作。
所以,0 是在中间的,前面 1、2、3和后面7、8、9,顺序未知(依赖于任务复杂度和cpu的调度)
接着看下面的题目
下面的代码打印什么?
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
上面的结果是1 5 2
,然后死锁崩溃
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9fAnpBP5-1588931804983)(https://user-gold-cdn.xitu.io/2020/5/8/171f3abb629de0e6?w=603&h=316&f=png&s=28382)]
分析:
首先是串行队列,先打印1,然后是异步并发,耗时操作, 先打印 5,然后将打
印 2 的任务加到队列,然后将代码块加入队列,再将打印4 的任务加入队列,然后将同步
打印3 的任务加入队列,
而任务 4 的执行,必须在代码块执行完之后,而代码块是同步,必须等任务3执行完,
而任务 3,在等待任务 4 执行完,就造成了相互等待,死锁的问题
同理,将打印 4,放到同步函数前,一样会死锁
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
NSLog(@"4");
dispatch_sync(queue, ^{
NSLog(@"3");
});
});
NSLog(@"5");