iOS -- 浅谈多线程原理

1,594 阅读10分钟

进程与线程

如果把进程比作是一个电子厂,那么线程就是一条条的流水作业线。电子厂与电子厂之间相互独立,当前电子厂的作业流水线只能使用自己电子厂资源。

进程

  • 进程是指在系统中正在运行的一个应用程序,比如打开的Xcode
  • 每个进程之间是独立的,每个进程运行在专有的而且受保护的内存空间中。

线程

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
  • 进程想要执行任务,必须要有线程,进程至少要有一条线程用来执行任务。
  • 程序启动时会默认开启一条线程,这条线程被称为主线程或者UI线程。

进程与线程的关系

  1. 线程是进程的执行单元,进程的所有任务都在线程中执行,同一个进程内的线程共享进程资源。
  2. 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  3. 资源拥有:同一进程内的线程共享本进程的资源如内存、I/Ocpu等,但是进程之间的 资源是独立的。
  4. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进 程都死掉。所以多进程要比多线程健壮。
  5. 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
  6. 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  7. 线程是处理器调度的基本单位,但是进程不是。
  8. 线程没有地址空间,线程包含在进程地址空间中。

多线程

多线程原理

我们知道一个进程可以开启多个线程,进程的所有任务都在线程中执行,而一个线程中的任务是串行的,如果要在一个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,一个线程只能执行一个任务,而在同一时刻,一个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):(互斥锁)。
  • 死亡:正常死亡,线程执行完毕。非正常死亡,当满足某个条件后,在线程内部中止执行(或者在主线程中止线程对象)

关于线程的exitcancel

[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又称为原子锁)。

atomicnonatomic

atomic 原子属性,是默认属性,是线程安全的,保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以取值。使用其需要消耗大量的资源。

nonatomic 非原子属性,是非线程安全的,同一时间可以有很多线程读和写。相比atomic效率更高。

iOS开发的过程中,建议将所有属性都声明为nonatomic,开发过程中尽量避免多线程抢夺同一资源,将资源的业务逻辑交由服务端完成。

线程之间的通信

在苹果的文档Threading Programming Guide文档的Table 1-3 Communication mechanisms部分,有提到关于线程之间通信的方式。

截屏2021-06-18 下午2.16.46.png 简单用代码介绍一下常用的通信方式:

  1. 直接消息: 通过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(@"完成");
}
  1. 端口通信
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

多线程的实现方式

多线程的四种实现方式分别是:pthreadNSThreadGCDNSOperationimage.png 下面通过代码来看一下这四种实现方式:

  1. 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];
  1. GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self threadTest];
});
  1. NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
        [self threadTest];
}];

- (void)threadTest{
    NSLog(@"begin");
    NSLog(@"over");
}

未完待续......