iOS

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

2021-08-11  本文已影响0人  俊而不逊

回顾

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

在这里插入图片描述

1. 线程的生命周期

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

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

线程生命周期
@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. 线程池的运行策略

线程池运行策略

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

线程池运行策略

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

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

饱和策略

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

3. 自旋锁和互斥锁

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

优先级翻转:

优先级因素:

    // 主线程 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

自旋锁和互斥锁的特点

自旋锁优缺点

原子属性和非原子属性

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

nonatomicatomic的对比

平时开发需要注意

atomic底层实现自旋锁

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

objc_setProperty
底层会调用reallySetProperty方法,在该方法的实现中,针对原子属性,添加了spinlock
void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}
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
更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

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

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

上一篇下一篇

猜你喜欢

热点阅读