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

回顾 \color{red}{}

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

1. 线程的\color{red}{生命周期}

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

  • 线程生命周期

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

\color{red}{新建}:通过创建线程的函数方法,创建一个新的线程。
\color{red}{就绪}:线程创建完成之后,调用 start方法,线程这个时候处于等待状态,等待CPU时间分配执行。
\color{red}{运行}:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能。
\color{red}{阻塞}:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep、等待同步锁,线程就从可调度线程池移出,处于了阻塞状态,这个时候sleep到时、获取同步锁,此时会重新添加到可调度线程池。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态。
\color{red}{销毁}:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。

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


  • 线程状态演练方法
@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密集型(很少等待)
  • IO 比CPU更容易得到优先级的提升
  • 饿死:一直等不到执行,就丢弃了
  • 调度:优先级和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窗口卖同一张票,这样就会出现问题,所有锁的意义重大了。

自旋锁

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

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

\color{red}{自旋锁}:OSSpinLock、dispatch_semaphore_t

互斥锁

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

自旋锁和互斥锁的特点

\color{red}{自旋锁}会忙等,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
\color{red}{互斥锁}会休眠,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作,直到被锁资源释放锁。此时会唤醒休眠线程。

\color{red}{自旋锁优缺点}

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

原子属性和非原子属性

\color{red}{OC}在定义属性时有\color{red}{nonatomic}\color{red}{atomic}两种选择,默认为\color{red}{atomic}属性

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

\color{red}{nonatomic}\color{red}{atomic}的对比

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

平时开发需要注意

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

\color{red}{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技术方案

多线程有Pthread、NSThread、GCD、NSOperation 等方案。

iOS技术方案如下图:

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

iOS底层探索之多线程(三)—初识GCD

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹
🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹
🌹 欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹
🌹 作者邮件:zhangxmsy@163.com 有问题联系。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容