iOS 锁的底层原理

Posted WeaterMr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 锁的底层原理相关的知识,希望对你有一定的参考价值。

@synchronized(互斥锁) 原理

1.clang分析实现原理

  
        objc_sync_enter(_sync_obj);
        try 
            // 结构体
            struct _SYNC_EXIT 
            // 构造函数
            _SYNC_EXIT(id arg) : sync_exit(arg) 
            // 析构函数
            ~_SYNC_EXIT() objc_sync_exit(sync_exit);
             id sync_exit;
            
           _sync_exit(_sync_obj);
        
    

分析:

  • 1.objc_sync_enter(_sync_obj);
  • 2._SYNC_EXIT 结构体
  • 3._sync_exit(_sync_obj);

objc_sync_enter 函数探索

int objc_sync_enter(id obj)
    int result = OBJC_SYNC_SUCCESS;
    if (obj) 
        // obj 存在
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
     else 
        // obj 不存在 什么事情也不做
    
    return result;

分析:

  • 1.被锁对象obj 为空的时候都是什么事情也不会做。
  • 2.核心函数id2data (两个状态参数:ACQUIRERELEASE
  • 3.SyncData 的数据结构。

SyncData 数据结构

typedef struct alignas(CacheLineSize) SyncData 
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;     // 使用此块的线程数
    recursive_mutex_t mutex; //递归锁
 SyncData;

分析:

  • 1.nextData 判断为单向链表,头插法。
  • 2.threadCount 多线程标记
  • 3.mutex 递归锁

id2data 函数实现分析

struct SyncList 
    SyncData *data;
    spinlock_t lock;
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock)  
;
// 使用多个并行列表来减少不相关对象之间的争用
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
// 全局静态变量   哈希表
static StripedMap<SyncList> sDataLists;
// 哈希函数 确定一个下标
// 可能会发生冲突 (之前通过在哈希来解决)
// 这里 使用 拉链法
...

static SyncData* id2data(id object, enum usage why)

    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
#if SUPPORT_DIRECT_THREAD_KEYS
    //支持线程局部存储
    
    // 检查每个线程的单条目快速缓存是否匹配对象
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    
    // 第一部分
    if (data)  ... 
    
#endif
    
    // 检查已经拥有的锁的每个线程缓存是否匹配对象
    SyncCache *cache = fetch_cache(NO);
    
    // 第二部分
    if (cache)  ... 
    
    //线程缓存没有找到任何东西
    //遍历正在使用的列表以查找匹配的对象
    //自旋锁防止多个线程创建多个
    //锁定相同的新对象
    //我们可以把节点保存在某个哈希表中,如果有的话 
    //有20多个不同的锁在工作中,但我们现在不这么做。
    
    
    //这里的锁 是为了保证在开辟内存空间时候的安全, 和外面的锁不一样哦, 此处是一个 spinlock_t 在上面有定义
    lockp->lock(); 
    
    // 第三部分
    // 代码块
     ... 
    
    // 分配一个新的SyncData并添加到列表中.
    // XXX在持有全局锁的情况下分配内存是不好的做法,
    // 可能值得释放锁,重新分配,再搜索一次.
    // 但由于我们从不释放这些人我们不会经常陷入分配中.
    
    // 第四部分
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData)); 
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
    
done:

    lockp->unlock(); //这里的锁 是为了保证在开辟内存空间时候的安全, 和外面的锁不一样哦,此处是一个 spinlock_t 在上面有定义
    
    // 第五部分
    if (result)  ... 
    
    return result;

  • 1: 创建一张全局的哈希表(static StripedMap sDataLists;), 使用拉链法来存储 SyncData;这里的全局表中存放不同对象,拉链拉的是通过hash计算得到的相同索引与相同对象的不同线程SyncData
  • 2: sDataLists, 中array 存储的是 SyncList (绑定的是objc,我们加锁的对象)
  • 3: objc_sync_enterobjc_sync_exit 调用对称,封装的是递归锁
  • 4: 两种存储 :TLS(线程局部存储) / cache(线程缓存)
  • 5: 第一次 syncData 头插法 链表 标记 threadCount = 1
  • 6: 判断是否同一个对象:如果是,lockCount++
  • 7: TLS找到 -> lock ++
  • 8: TLS 找不到 sync threadCount ++
  • 9: lock — threadCount—
    Synchronized : 可重入 递归 多线程
  • 1: TLS 保障 threadCount 多少条线程对这个锁对象加锁
  • 2: lock ++ 进来多少次

NSCondition和NSConditionLock

NSCondition
条件锁,顾名思义,就是满足某些条件才会开锁。NSCondition,可以确保线程仅在满足特定条件时才能获取锁。一旦获得了锁并执行了代码的关键部分,线程就可以放弃该锁并将关联条件设置为新的条件。条件本身是任意的:可以根据应用程序的需要定义它们。

NSCondition对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。通俗的说,也就是条件成立,才会执行锁住的代码。条件不成立时,线程就会阻塞,直到另一个线程向条件对象发出信号解锁为止。

下面我们看一个例子:

- (void)conditionTest 
    for (int i = 0; i < 50; i++) 
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^
            [self addTickets];
        );
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^
            [self addTickets];
        );
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^
            [self minusTickets];
        );
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^
            [self minusTickets];
        );
    


- (void)addTickets 
    self.ticketCount += 1;
    NSLog(@"加一个现有ticketCount==%zd",self.ticketCount);


- (void)minusTickets 
    while (self.ticketCount == 0) 
        NSLog(@"==没有ticketCount==");
        return;
    
    
    self.ticketCount -= 1;
    NSLog(@"减一个剩下ticketCount==%zd",self.ticketCount);

运行程序,我们发现控制台的输出是有问题的

此时,我们就可以使用条件锁解决问题。我们只需要对程序作如下改动就可以正常执行:

- (void)addTickets 
    [self.condition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"现有ticketCount==%zd",self.ticketCount);
    [self.condition unlock];
    [self.condition signal];


- (void)minusTickets 
    [self.condition lock];
    while (self.ticketCount == 0) 
        NSLog(@"==没有ticketCount==");
        [self.condition wait];
        return;
    
    
    self.ticketCount -= 1;
    NSLog(@"减一个,剩下ticketCount==%zd",self.ticketCount);
    [self.condition unlock];

需要使用swift源码进行查看:

open class NSCondition: NSObject, NSLocking 
    internal var mutex = _MutexPointer.allocate(capacity: 1)
    internal var cond = _ConditionVariablePointer.allocate(capacity: 1)

    public override init() 
        pthread_mutex_init(mutex, nil)
        pthread_cond_init(cond, nil)
    
    
    deinit 
        pthread_mutex_destroy(mutex)
        pthread_cond_destroy(cond)
        mutex.deinitialize(count: 1)
        cond.deinitialize(count: 1)
        mutex.deallocate()
        cond.deallocate()
    
    
    // 一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,
    // 其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
    open func lock() 
        pthread_mutex_lock(mutex)
    
    
    // 释放锁,与lock成对出现
    open func unlock() 
        pthread_mutex_unlock(mutex)
    
    
    // 让当前线程处于等待状态,阻塞
    open func wait() 
        pthread_cond_wait(cond, mutex)
    

    // 让当前线程等待到某个时间,阻塞
    open func wait(until limit: Date) -> Bool 
        guard var timeout = timeSpecFrom(date: limit) else 
            return false
        
        return pthread_cond_timedwait(cond, mutex, &timeout) == 0
    
    
    // 发信号告诉线程可以继续执行,唤醒线程
    open func signal() 
        pthread_cond_signal(cond)
    
    
    open func broadcast() 
        pthread_cond_broadcast(cond) // wait  signal
    
    
    open var name: String?

可以看到,该对象还是对pthread_mutex的一层封装,NSCondition也是一种互斥锁。当我们需要等待某个条件的时候,也就是条件不满足的时候,就可以使用wait方法来阻塞线程,当条件满足了,使用signal方法发送信号唤醒线程。

NSConditionLockNSCondition又做了一层封装,自带条件探测,能够更简单灵活的使用。

我们使用swift查看一下:

internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
    
public convenience override init() 
    self.init(condition: 0)

    
public init(condition: Int) 
    _value = condition


// 表示 xxx 期待获得锁,
// 如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,
// 如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
open func lock() 
    let _ = lock(before: Date.distantFuture)


open func unlock() 
    _cond.lock()
    _thread = nil
    _cond.broadcast()
    _cond.unlock()

    
open var condition: Int 
    return _value


// 表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。
// 如果内部的condition等于A条件,并且没有其他线程获得该锁,则执行任务,同时设置它获得该锁
// 其他任何线程都将等待它代码的完成,直至它解锁。
open func lock(whenCondition condition: Int) 
    let _ = lock(whenCondition: condition, before: Date.distantFuture)


open func `try`() -> Bool 
    return lock(before: Date.distantPast)

    
open func tryLock(whenCondition condition: Int) -> Bool 
    return lock(whenCondition: condition, before: Date.distantPast)


// 表示释放锁,同时把内部的condition设置为A条件
open func unlock(withCondition condition: Int) 
    _cond.lock()
    _thread = nil
    _value = condition
    _cond.broadcast()
    _cond.unlock()


open func lock(before limit: Date) -> Bool 
    _cond.lock()
    while _thread != nil 
        if !_cond.wait(until: limit) 
            _cond.unlock()
            return false
        
    
    _thread = pthread_self()
    _cond.unlock()
    return true

    
// 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。
// 需要注意的是:返回的值是NO,它没有改变锁的状态,这个函 数的目的在于可以实现两种状态下的处理
open func lock(whenCondition condition: Int, before limit: Date) -> Bool 
    _cond.lock()
    while _thread != nil || _value != condition 
        if !_cond.wait(until: limit) 
            _cond.unlock()
            return false
        
    
    _thread = pthread_self()
    _cond.unlock()
    return true

    
open var name: String?

可以看出,触发的唤醒线程的条件是传入的condition取值,和我们创建锁的时候值要相同,我们可以在释放当前线程锁的时候重新设置其他线程传入的condition值,这样也就达到了唤醒其他线程的目的。如果创建锁的值和传入的值都不能匹配,则会进入阻塞状态。

下面我们来看个例子:

- (void)conditionLockTest 
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^
       [conditionLock lockWhenCondition:1];
       NSLog(@"线程1");
       [conditionLock unlockWithCondition:0];
    );
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^
       [conditionLock lockWhenCondition:2];
       NSLog(@"线程2");
       [conditionLock unlockWithCondition:1];
    );
    dispatch_async(dispatch_get_global_queue(0, 0), ^
       [conditionLock lock];
       NSLog(@"线程3");
       [conditionLock unlock];
    );

执行流程首先搞明白传入的2的作用,这个值只是一个初始条件,
他的配套使用方法是lockWhenCondition通过判断条件是否相等来决定是否等待,并且在使用过程中这个条件可以更改,从而达到按照一定顺序执行任务的目的。如果不使用条件将不受条件控制。
总结
相同点:
都是互斥锁
通过条件变量来控制加锁、释放锁,从而达到阻塞线程、唤醒线程的目的
不同点:

NSCondition是基于对pthread_mutex的封装,而NSConditionLock是对NSCondition做了一层封装
NSCondition是需要手动让线程进入等待状态阻塞线程、释放信号唤醒线程,NSConditionLock则只需要外部传入一个值,就会依据这个值进行自动判断是阻塞线程还是唤醒线程

以上是关于iOS 锁的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

iOS之深入解析malloc的底层原理

iOS底层探索之多线程(十三)—锁的种类你知多少?

epoll底层原理总结

iOS 底层原理之alloc探究

[iOS开发]weak底层原理

synchronized 底层如何实现?啥是锁的升级,降级