ios底层探索之多线程—线程和锁(代码片段)

卡卡西Sensei 卡卡西Sensei     2022-12-20     737

关键词:

回顾

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

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 替换成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源码)

底层会调用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技术方案如下图:

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

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

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

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

ios底层探索之多线程—gcd源码分析(栅栏函数)(代码片段)

...顾在上篇博客已经对GCD函数的同步性/异步性还有单例的底层源码,作了详细的分析,那么本篇博客将对栅栏函数,调度组等底层源码进行探索分析!iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)... 查看详情

ios底层探索之多线程(十四)—关于@synchronized锁你了解多少?(代码片段)

...f;对于锁你又了解多少?锁的原理你又知道吗?iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)—线程和锁iOS底层探索之多线程(三)—初识GCDiOS底层探索之多线程(四)—GCD的队列iOS底层探索之多线程(五)—GCD... 查看详情

ios底层探索之多线程(十三)—锁的种类你知多少?(代码片段)

...#xff1f;从本篇博客开始将对锁的相关内容进行分析!iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)—线程和锁iOS底层探索之多线程(三)—初识GCDiOS底层探索之多线程(四)—GCD的队列iOS底层探索之多线程(五)—GCD... 查看详情

ios底层探索之多线程—gcd的队列(代码片段)

...的认识,那么本篇博客将继续介绍GCD的相关知识。iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)—线程和锁iOS底层探索之多线程(三)—初识GCD1.不同队列举例主队列添加同步任务看看下面这个例子🌰//主队... 查看详情

ios底层探索之多线程—gcd不同队列源码分析(代码片段)

...,那么本篇博客将继续介绍GCD的队列和源码分析。iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)—线程和锁iOS底层探索之多线程(三)—初识GCDiOS底层探索之多线程(四)—GCD的队列1.主队列分析查看主队列的api如... 查看详情

ios底层探索之多线程—gcd源码分析(函数的同步性异步性单例)(代码片段)

...博客已经对GCD的sync同步函数产生死锁的情况,进行了底层的源码探索分析,那么本篇博客继续源码的探索分析!iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)—线程和锁iOS底层探索之多线程(三)—初... 查看详情

ios底层探索之多线程—gcd源码分析(信号量dispatch_semaphore_t)(代码片段)

...栅栏函数做了一个基本介绍,还有应用的举例并且对底层源码进行了分析,本篇博客将对信号量进行探索分析!iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)—线程和锁iOS底层探索之多线程(三)—初识... 查看详情

ios底层探索之多线程—gcd源码分析(事件源dispatch_source)(代码片段)

...博客已经对GCD的调度组做了介绍和举例应用,还有对底层源码的分析,那么本篇博客将对事件源dispatch_source进行分析!iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)—线程和锁iOS底层探索之多线程(三)... 查看详情

ios底层探索之多线程—gcd源码分析(调度组)(代码片段)

...已经对GCD的信号量做了一个介绍和举例应用,还有对底层源码的分析,那么本篇博客看苹果工程师,如何巧妙封装调度组,看完底层源码直呼好家伙,真是妙啊!!!iOS底层探索之多线程(一)—进程... 查看详情

ios底层探索之多线程—初识gcd(代码片段)

...用最多的还是GCD,那么从本篇开始讲陆续介绍GCD。iOS底层探索之多线程(一)—进程和线程iOS底层探索之多线程(二)—线程和锁1.什么是GCDGCD定义GrandCenterDispatch简称GCD,是苹果公司开发的技 查看详情

ios开发底层之多线程探索-19(代码片段)

文章目录前言一、进程与线程?1.进程2.线程3.进程与线程的关系二、多线程1.多线程优点2.多线程缺点3.线程的生命周期4.线程池的饱和策略RejectedExecutionHandler接口5.优先级翻转(IOvccpu优先级提升)6.优先级的影响因素三.多线程下... 查看详情

ios开发底层之多线程探索-19(代码片段)

文章目录前言一、进程与线程?1.进程2.线程3.进程与线程的关系二、多线程1.多线程优点2.多线程缺点3.线程的生命周期4.线程池的饱和策略RejectedExecutionHandler接口5.优先级翻转(IOvccpu优先级提升)6.优先级的影响因素三.多线程下... 查看详情

ios底层探索之多线程—进程和线程(代码片段)

前言在iOS的面试中多线程是经常被问到的,多线程也是一个难点,很多面试者平时用的不多,因此很难回答到点子上,那么本篇博客就对多线程进行探索和分析。1.进程和线程什么是进程进程是指在系统中正在运... 查看详情

ios底层探索之多线程(十六)——锁分析(nslocknscondtionnsrecursivelocknscondition)(代码片段)

...绍,那么本篇博客就分析一下其他的一些锁!iOS底层探索之多线程(一)— 查看详情

ios底层探索之多线程(十七)——通过swift的foundation源码分析锁(nslocknsconditionnsrecursivelock)(代码片段)

...篇博客就继续分析锁,从Foundation源码分析锁!iOS底层探索之多线程(一)—进程和线程 查看详情

ios底层探索之多线程—gcd源码分析(死锁的原因)(代码片段)

回顾在上篇博客已经对GCD的sync同步函数、async异步函数进行了源码的分析,那么本篇博客继续源码的探索分析!1.补充sync和async的区别是否可以开启新的线程执行任务任务的回调是否具有异步行、同步性是否产生死锁问题... 查看详情

ios底层探索之多线程—gcd源码分析(sync同步函数async异步函数)(代码片段)

回顾在上篇博客对GCD的不同的队列继续了底层的源码探索分析,那么本篇博客将继续对GCD的函数继续源码分析。1.sync同步函数我们都知道GCD底层是用C写的,封装了block函数来执行添加的任务,那么这个block底层是如何... 查看详情

ios底层探索之多线程(十八)——锁篇章的完结篇(手把手两种方式带你实现一个读写锁!)(代码片段)

...,那么本篇博将手把手带你实现一个读写锁!iOS底层探索之多线程(一)—进程和线程iOS 查看详情