美文网首页iOS
iOS底层探索之多线程(二)—线程和锁

iOS底层探索之多线程(二)—线程和锁

作者: 俊而不逊 | 来源:发表于2021-08-11 13:20 被阅读0次

    回顾

    上一篇博客中,我们已经对进程和线程有了一定的了解了,那么本次博客将继续讲解!

    在这里插入图片描述

    1. 线程的生命周期

    在程序开发中有个名词——生命周期,我们都知道APP 有生命周期,那么线程的生命周期是什么样子的呢?

    • 线程生命周期

    线程生命周期大致包括 5个阶段:

    • 新建:通过创建线程的函数方法,创建一个新的线程。

    • 就绪:线程创建完成之后,调用 start方法,线程这个时候处于等待状态,等待CPU时间分配执行。

    • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能。

    • 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep等待同步锁,线程就从可调度线程池移出,处于了阻塞状态,这个时候sleep到时、获取同步锁,此时会重新添加到可调度线程池。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态。

    • 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。

    • 线程生命周期大致流程图如下:

    线程生命周期
    • 线程状态演练方法
    @interface ViewController ()
    @property (nonatomic, strong) NSThread *p_thread;
    @end
    
    /**
     线程状态演练方法
     */
    - (void)testThreadStatus{
        NSLog(@"%d %d %d", self.p_thread.isExecuting, self.p_thread.isFinished, self.p_thread.isCancelled);
        // 生命周期
        
        if ( self.p_thread == nil || self.p_thread.isCancelled || self.p_thread.isFinished ) {
            self.p_thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
            self.p_thread.name = @"跑步线程";
            [self.p_thread start];
        }else{
            NSLog(@"%@ 正在执行",self.p_thread.name);
            
            //可以设置弹框 ---> 这里直接制空
            [self.p_thread cancel];
            self.p_thread = nil;
        }
    }
    

    2. 线程池的运行策略

    线程池运行策略

    线程的工作执行,也是有一定的策略的,线程池的运行策略见下图:

    线程池运行策略

    队列满了且正在运行的线程数量,小于最大线程数,则新进来的任务,会直接创建非核心线程来完成工作。

    • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

    当有任务时,线程池会做如下判断:

    • 如果正在运行的线程数量小于corePoolSize(核心线程数),那么马上创建核心线程运行这个任务。
      如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列。
    • 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize(最大线程数),那么还是要创建非核心线程立刻运行这个任务。
    • 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池饱和策略将进行处理。
    • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
    • 当一个线程无事可做,超过一定的时间(超时)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

    饱和策略

    如果线程池中的队列满了,并且正在运行的线程数量已经大于等于当前线程池的最大线程数,则进行饱和策略的处理。

    • AbortPolicy直接抛出RejectedExecutionExeception异常来阻⽌系统正常运⾏
    • CallerRunsPolicy将任务回退到调⽤者
    • DisOldestPolicy丢掉等待最久的任务
    • DisCardPolicy直接丢弃任务

    3. 自旋锁和互斥锁

    任务的执行速度的影响因素:

    • CPU
    • 任务的复杂度
    • 任务的优先级
    • 线程的状态

    优先级翻转:

    • IO 密集型(频繁的等待线程)
    • CPU密集型(很少等待)
    • IOCPU更容易得到优先级的提升
    • 饿死:一直等不到执行,就丢弃了
    • 调度:优先级和CPU的调度还有关系

    优先级因素:

    • 用户指定优先级 --> threadPriority
        // 主线程 512K
        NSLog(@"%@ %zd K %d", [NSThread currentThread], [NSThread currentThread].stackSize / 1024, [NSThread currentThread].isMainThread);
        NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(eat) object:nil];
        // 1. name - 在应用程序中,收集错误日志,能够记录工作的线程!
        // 否则不好判断具体哪一个线程出的问题!
        t.name = @"吃饭线程";
        //This value must be in bytes and a multiple of 4KB.
        t.stackSize = 1024*1024;
        t.threadPriority = 1;
        [t start];
        
    

    threadPriority

    threadPriority

    threadPriority 替换成qualityOfService(NSQualityOfService)

    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));
    
    
    • 等待的频繁度
    • 长时间不执行(也会提升优先级)

    下图是:一个线程的经典案例


    线程经典案例

    车票售卖系统,是线程工作执行的经典案例,如果是多个窗口卖票,会出现资源抢夺的情况,如果 A窗口卖了一张票,B窗口不知道,或者同一时间 AB窗口卖同一张票,这样就会出现问题,所有锁的意义重大了。

    自旋锁

    是一种用于保护多线程共享资源的锁,与一般互斥锁mutex)不同之处在于当自旋锁尝试获取锁时,以忙等待(busy waiting)的形式不断地循环检查锁是否可用;当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠);当上一个线程的任务执行完毕,下一个线程会立即执行。

    在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

    自旋锁OSSpinLock、dispatch_semaphore_t

    互斥锁

    当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务,该任务也不会立刻执行,而是成为可执行状态(就绪)。
    互斥锁pthread_mutex、@ synchronized、NSLock、NSConditionLock、NSCondition、NSRecursiveLock

    自旋锁和互斥锁的特点

    • 自旋锁会忙等,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。

    • 互斥锁会休眠,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作,直到被锁资源释放锁。此时会唤醒休眠线程。

    自旋锁优缺点

    • 优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
    • 缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。

    原子属性和非原子属性

    OC在定义属性时有nonatomicatomic两种选择,默认为atomic属性

    • atomic:原子属性,为setter方法加自旋锁(即为单写多读)
    • nonatomic:非原子属性,不会为setter方法加锁

    nonatomicatomic的对比

    • atomic:线程安全,需要消耗大量的资源;
    • nonatomic:非线程安全,适合内存小的移动设备。

    平时开发需要注意

    • 如非需抢占资源的属性(如购票,充值),所有属性都声明为nonatomic
    • 尽量避免多线程抢夺同一块资源。
    • 尽量将加锁资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。

    atomic底层实现自旋锁

    我们在探索类的本质时,对于类的属性的setter方法,系统会有一层objc_setProperty的封装(libobjc.dylib源码)

    objc_setProperty
    底层会调用reallySetProperty方法,在该方法的实现中,针对原子属性,添加了spinlock
    • objc_setProperty_atomic_copy
    void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
    {
        reallySetProperty(self, _cmd, newValue, offset, true, true, false);
    }
    
    • reallySetProperty
    static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
    {
        if (offset == 0) {
            object_setClass(self, newValue);
            return;
        }
    
        id oldValue;
        id *slot = (id*) ((char*)self + offset);
    
        if (copy) {
            newValue = [newValue copyWithZone:nil];
        } else if (mutableCopy) {
            newValue = [newValue mutableCopyWithZone:nil];
        } else {
            if (*slot == newValue) return;
            newValue = objc_retain(newValue);
        }
    
        if (!atomic) {
            oldValue = *slot;
            *slot = newValue;
        } else {
            spinlock_t& slotlock = PropertyLocks[slot];
            slotlock.lock();
            oldValue = *slot;
            *slot = newValue;        
            slotlock.unlock();
        }
    
        objc_release(oldValue);
    }
    

    SpinlockLinux内核中提供的一种比较常见的锁机制,自旋锁是原地等待的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地打转(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗CPU资源)。

    纠正一下

    atomic只是原子属性的一个标识符,所以atomic并不是自旋锁,底层是通过Spinlock实现自旋锁。

    线程和Runloop的关系

    1. runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局
      的字典里。
    2. runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休
      眠状态,有了任务就会被唤醒去执行任务。
    3. runloop在第一次获取时被创建,在线程结束时被销毁。
    4. 对于主线程来说,runloop在程序一启动就默认创建好了。
    5. 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。

    4. iOS技术方案

    多线程有PthreadNSThreadGCDNSOperation 等方案。

    iOS技术方案如下图:


    iOS技术方案

    关于多线程的更多信息,可以去苹果文档去看看
    Threading Programming Guide

    Threading Programming Guide
    更多内容持续更新

    🌹 喜欢就点个赞吧👍🌹

    🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

    🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

    相关文章

      网友评论

        本文标题:iOS底层探索之多线程(二)—线程和锁

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