美文网首页
iOS -- 浅谈多线程原理

iOS -- 浅谈多线程原理

作者: iOS开发之家 | 来源:发表于2021-06-22 16:59 被阅读0次

    进程与线程

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

    进程

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

    线程

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

    进程与线程的关系

    1. 线程是进程的执行单元,进程的所有任务都在线程中执行,同一个进程内的线程共享进程资源。
    2. 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
    3. 资源拥有:同一进程内的线程共享本进程的资源如内存、I/Ocpu等,但是进程之间的 资源是独立的。
    4. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进 程都死掉。所以多进程要比多线程健壮。

    作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

    如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料: BAT 大厂最新面试题+答案合集(持续更新中) 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障

    1. 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
    2. 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
    3. 线程是处理器调度的基本单位,但是进程不是。
    4. 线程没有地址空间,线程包含在进程地址空间中。

    多线程

    多线程原理

    我们知道一个进程可以开启多个线程,进程的所有任务都在线程中执行,而一个线程中的任务是串行的,如果要在一个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,一个线程只能执行一个任务,而在同一时刻,一个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

    多线程的实现方式

    多线程的四种实现方式分别是:pthreadNSThreadGCDNSOperation

    image.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");
    }
    

    未完待续......

    作者:Henry_Jeannie
    链接:https://juejin.cn/post/6975035560607875080
    来源:掘金

    相关文章

      网友评论

          本文标题:iOS -- 浅谈多线程原理

          本文链接:https://www.haomeiwen.com/subject/xgrmyltx.html