iOS -- 浅谈多线程原理
进程与线程
如果把进程比作是一个电子厂,那么线程就是一条条的流水作业线。电子厂与电子厂之间相互独立,当前电子厂的作业流水线只能使用自己电子厂资源。
进程
- 进程是指在系统中正在运行的一个应用程序,比如打开的
Xcode
。 - 每个进程之间是独立的,每个进程运行在专有的而且受保护的内存空间中。
线程
- 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
- 进程想要执行任务,必须要有线程,进程至少要有一条线程用来执行任务。
- 程序启动时会默认开启一条线程,这条线程被称为主线程或者
UI
线程。
进程与线程的关系
- 线程是进程的执行单元,进程的所有任务都在线程中执行,同一个进程内的线程共享进程资源。
- 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
- 资源拥有:同一进程内的线程共享本进程的资源如内存、
I/O
、cpu
等,但是进程之间的 资源是独立的。 - 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进 程都死掉。所以多进程要比多线程健壮。
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料: BAT 大厂最新面试题+答案合集(持续更新中) 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障
- 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
- 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 线程是处理器调度的基本单位,但是进程不是。
- 线程没有地址空间,线程包含在进程地址空间中。
多线程
多线程原理
我们知道一个进程可以开启多个线程,进程的所有任务都在线程中执行,而一个线程中的任务是串行的,如果要在一个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,一个线程只能执行一个任务,而在同一时刻,一个CPU
只能处理一条线程(只有一个线程在执行任务),但CPU
可以在多条线程之间快速的切换,只要切换的足够快,就造成了多线程一同执行的假象。
多线程的优缺点
优点
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(
CPU
,内存) - 线程上的任务执行完成后,线程会自动销毁
缺点
- 开启线程需要占用一定的内存空间(默认情况下,主线程占用
1 MB
,子线程都占用512 KB
) - 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,
CPU
在调用线程上的开销就越大 - 程序设计更加复杂,比如线程间的通信、多线程的数据共享
那么提出一个疑问❓如果进程开启的线程非常非常多,会发生什么情况❓
答:CPU
会在许多线程之间调度,CPU
会累死,会消耗大量的CPU
资源, 而且每条线程被调度执行的频次会降低(线程的执行效率也就降低)
主线程(UI
线程)
一个iOS
程序运行后,默认会开启一条线程,称为主线程
或UI线程
.主线程主要用于显示刷新UI
界面,处理UI
事件。(最好不要将耗时任务放在主线程处理,耗时操作会卡住主线程,造成一种卡顿现象。)
线程的生命周期
image.png- 新建:实例化线程对象
- 就绪:向线程对象发送
start
消息,线程对象并不会立即执行,线程对象被加入可调度线程池等待CPU
调度。 - 运行:
CPU
负责调度可调度线程池中线程的执行。线程执行完成之前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU
负责,程序员不能干预。 - 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行。当进入休眠时,会重新将线程加入就绪状态中。休眠的时间设置参数为:
sleepForTimeInterval
(休眠指定时长),sleepUntilDate
(休眠到指定日期),@synchronized(self)
:(互斥锁)。 - 死亡:正常死亡,线程执行完毕。非正常死亡,当满足某个条件后,在线程内部中止执行(或者在主线程中止线程对象)
关于线程的exit
和cancel
:
[NSThread exit]:一旦强行终止线程,后续的所有代码都不会执行
[thread cancel]:并不会直接取消正在执行的线程,只是给线程对象添加 isCancelled 标记
线程的优先级
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
复制代码
上述优先级从高到低,但是,线程执行的快慢,除了看线程的优先级,还需要查看执行任务资源的大小(即任务的复杂度)、以及 CPU
调度情况。
线程池
image.png线程安全
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。就好像售票系统,如果多人同时在售票,每个人的售票处理的速度不一样,那么就会造成余票的数量飘忽不定。 那么解决多线程安全问题有两种方法:互斥锁和自旋锁。
互斥锁和自旋锁
互斥锁(同步锁):@synchronized
@synchronized(锁对象) {
// 需要锁定的代码
}
复制代码
- 用于保护临界区,保证锁内的代码,同一时间,只有一条线程能够执行。
- 判断的时候锁对象要存在,如果代码中只有一个地方需要加锁,大多都使用
self
作为锁对象,这样可以避免单独再创建一个锁对象。 - 加了互斥锁的代码,当有新的线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠。
- 锁对象一定要保证所有的线程都能够访问。
- 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差。
自旋锁
自旋锁不同于互斥锁通过线程休眠来达到阻塞,自旋锁是线程在获取锁对象之前,一直处于忙等询问的阻塞状态。
加了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方式,一直等待锁定的代码执行完成。相当于不停尝试执行代码,比较消耗性能。其中,属性修饰符atomic
,本身就有一把自旋锁(atomic
又称为原子锁)。
atomic
和nonatomic
atomic
原子属性,是默认属性,是线程安全的,保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以取值。使用其需要消耗大量的资源。
nonatomic
非原子属性,是非线程安全的,同一时间可以有很多线程读和写。相比atomic
效率更高。
在iOS
开发的过程中,建议将所有属性都声明为nonatomic
,开发过程中尽量避免多线程抢夺同一资源,将资源的业务逻辑交由服务端完成。
线程之间的通信
在苹果的文档Threading Programming Guide文档的Table 1-3 Communication mechanisms
部分,有提到关于线程之间通信的方式。
简单用代码介绍一下常用的通信方式:
- 直接消息: 通过
performSelector
的一系列方法
//异步下载图像
[self performSelectorInBackground:@selector(downloadImageWithURL:) withObject:url];
- (void)downloadImageWithURL:(NSURL *)url {
// 1\. 获取二进制数据
NSData *data = [NSData dataWithContentsOfURL:url];
// 2\. 将二进制数据转换成 image
UIImage *image = [UIImage imageWithData:data];
// 3\. 在主线程更新 UI
// waitUntilDone: 是否等待 updateImage: 执行完成
[self performSelectorOnMainThread:@selector(updateImage:) withObject:image waitUntilDone:YES];
NSLog(@"完成");
}
复制代码
- 端口通信
ZhModel.h
@interface ZhModel : NSObject
- (void)modelLaunchThreadWithPort:(NSPort *)port;
@end
ZhModel.m
#import "ZhModel.h"
@interface ZhModel()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end
@implementation ZhModel
- (void)modelLaunchThreadWithPort:(NSPort *)port{
NSLog(@"VC 响应了Model里面");
@autoreleasepool {
//1\. 保存主线程传入的port
self.vcPort = port;
//2\. 设置子线程名字
[[NSThread currentThread] setName:@"ZhModelThread"];
//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 = [@"ZhModel" 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(@"model: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"]);
}
@end
复制代码
PortViewController.m
#import "PortViewController.h"
#import <objc/runtime.h>
#import "ZhModel.h"
@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) ZhModel *zhmodel;
@end
@implementation PortViewController
- (void)viewDidLoad {
[super viewDidLoad];
//1\. 创建主线程的port
// 子线程通过此端口发送消息给主线程
self.myPort = [NSMachPort port];
//2\. 设置port的代理回调对象
self.myPort.delegate = self;
//3\. 把port加入runloop,接收port消息
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
self.zhmodel = [[ZhModel alloc] init];
[NSThread detachNewThreadSelector:@selector(modelLaunchThreadWithPort:)
toTarget:self.zhmodel
withObject:self.myPort];
}
#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"VC == %@",[NSThread currentThread]);
NSLog(@"从person 传过来一些信息:");
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(@"Thread == %@",[NSThread currentThread]);
BOOL success = [destinPort sendBeforeDate:[NSDate date]
msgid:10010
components:array
from:self.myPort
reserved:0];
NSLog(@"%d",success);
}
@end
复制代码
截屏2021-06-18 下午3.01.00.png
多线程的实现方式
多线程的四种实现方式分别是:pthread
,NSThread
,GCD
, NSOperation
。
下面通过代码来看一下这四种实现方式:
pthread
/**
pthread_create 创建线程
参数:
1\. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
同时不需要 `*`
2\. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
3\. 线程要执行的`函数地址`
void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
(*): 函数名
(void *): 参数类型,void *
4\. 传递给第三个参数(函数)的`参数`
返回值:int
0 创建线程成功!成功只有一种可能
非 0 创建线程失败的错误码,失败有多种可能!
*/
pthread_t threadId = NULL;
char *cString = "HelloWorld";
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
NSLog(@"成功");
} else {
NSLog(@"失败");
}
void *pthreadTest(void *para){
// __bridge 将 C 语言的类型桥接到 OC 的类型
NSString *name = (__bridge NSString *)(para);
NSLog(@"===>%@ %@", [NSThread currentThread], name);
return NULL;
}
复制代码
2.NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
复制代码
GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self threadTest];
});
复制代码
NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
[self threadTest];
}];
- (void)threadTest{
NSLog(@"begin");
NSLog(@"over");
}
未完待续......
作者:Henry_Jeannie
链接:https://juejin.cn/post/6975035560607875080
来源:掘金