iOS底层学习 - 多线程之中的锁🔐

6,091 阅读19分钟

通过之前篇章的学习,我们对整个GCD从使用到原理,都有了一定的理解。这篇主要讲解一下iOS开发中的锁是什么情况

系列文章传送门:

iOS底层学习 - 多线程之基础原理篇

iOS底层学习 - 多线程之GCD初探

iOS底层学习 - 多线程之GCD队列原理篇

iOS底层学习 - 多线程之GCD应用篇

iOS底层学习 - 多线程之GCD底层原理篇

基础小概念

什么是锁

锁 -- 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

锁的作用

前面说到了,锁是用来保护线程安全的工具。

可以试想一下,多线程编程时,没有锁的情况 -- 也就是线程不安全。

当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:

程序执行的顺序会被打乱,可能造成提前释放一个变量,计算结果错误等情况。

所以我们需要将线程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (Synchronization)。

锁的分类

锁的分类方式,可以根据锁的状态,锁的特性等进行不同的分类,很多锁之间其实并不是并列的关系,而是一种锁下的不同实现。可以看这篇文章JAVA中锁的分类

互斥锁与自旋锁

互斥锁:是⼀种⽤于多线程编程中,防⽌两条线程同时对同⼀公共资源(⽐ 如全局变量)进⾏读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。 互斥锁又分为递归锁和非递归锁。

  • 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用。
  • 非递归锁:不可重入,必须等锁释放后才能再次获取锁。

⾃旋锁:线程反复检查锁变量是否可⽤。由于线程在这⼀过程中保持执⾏, 因此是⼀种忙等待。⼀旦获取了⾃旋锁,线程会⼀直保持该锁,直⾄显式释 放⾃旋锁。 ⾃旋锁避免了进程上下⽂的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。

互斥锁与自旋锁区别:

其实就是线程的区别,互斥锁在线程获取锁但没有获取到时,线程会进入休眠状态,等锁被释放时,线程会被唤醒,而自旋锁的线程则会一直处于等待状态,忙等待,不会进入休眠。

自旋锁

1. OSSpinLock

相信大家都拜读过这片文章->不再安全的 OSSpinLock。总结来说,自旋锁之所以不安全,是因为由于自旋锁获取锁时,线程会一直处于忙等待状态,造成了任务的优先级反转。

OSSpinLock 忙等的机制,就可能造成高优先级一直 running ,占用 CPU 时间片。而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。

2. atomic

在面试中,我们经常遇到关于atomic相关的问题,总结来说主要是两个方面,一个是atomic的底层原理是怎样的,另一个是使用atomic是否就能保证线程安全。

关于底层原理,我们还是来看源码进行探索。通过源码,我们可以发现,在方法的setget方法中,会有是否是atomic的判断,如果不是的话,则直接进行赋值,如果是的话,会加一个spinlock_t的锁,这个锁保证了对属性读写的安全。

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    // ...

    if (!atomic) {
        // 不是 atomic 修饰
        oldValue = *slot;
        *slot = newValue;
    } else {
        // 如果是 atomic 修饰,加一把同步锁,保证 setter 的安全
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
}

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    // ...
    
    // 非原子属性,直接返回值
    if (!atomic) return *slot;
    // 原子属性,加同步锁,保证 getter 的安全
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
}

既然atomic是保证setget方法安全的,那是不是就说明其线程安全呢?其实并不是的,这只能保证该属性在单一线程上是安全的,如果是有很多的线程对该属性进行同时的操作,那么就不能保证其数据安全了.比如下面的代码,通过结果我们可以看到,并没有起到加锁的效果。

    //Thread A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100; i ++) {
            self.num = self.num + 1;
            NSLog(@"Thread A:%ld\n",self.num);
        }
    });
    
    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100; i ++) {
            NSLog(@"Thread B:%ld\n",self.num);
        }
    });
    
-------------------------------------------------------------
Thread A:1
Thread B:1
Thread B:2
Thread A:2
    

3. 读写锁

读写锁实际是⼀种特殊的⾃旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进⾏读访问,写者则需要对共享资源进⾏写操作。这种锁相对于⾃旋锁⽽⾔,能提⾼并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最⼤可能的读者数为实际的逻辑CPU数。

  • 写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的。

  • 如果读写锁当前没有读者,也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须⾃旋在那⾥,直到写者释放该读写锁。

具体用法如下,不过在日常开发中较少使用

// 需要导入头文件
#include <pthread.h>

pthread_rwlock_t lock;
// 初始化锁
pthread_rwlock_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthread_rwlock_wrlock(&lock);
// 写-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁
pthread_rwlock_destroy(&lock);

我们可以使用并发队列+dispatch_barrier_async来实现一个类似的读写锁

########### .h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface WY_RWLock : NSObject
// 读数据
- (id)wy_objectForKey:(NSString *)key;
// 写数据
- (void)wy_setObject:(id)obj forKey:(NSString *)key;
@end

NS_ASSUME_NONNULL_END

########### .m文件
#import "WY_RWLock.h"

@interface WY_RWLock ()
// 定义一个并发队列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 多个线程需要数据访问
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
@end

@implementation WY_RWLock

- (id)init{
    self = [super init];
    if (self){
        // 创建一个并发队列:
        self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 创建数据字典:
        self.dataCenterDic = [NSMutableDictionary dictionary];
    }
    return self;
}

#pragma mark - 读数据
- (id)wy_objectForKey:(NSString *)key{
    __block id obj;
    // 同步读取指定数据:
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.dataCenterDic objectForKey:key];
    });
    return obj;
}

#pragma mark - 写数据
- (void)wy_setObject:(id)obj forKey:(NSString *)key{
    // 异步栅栏调用设置数据:
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.dataCenterDic setObject:obj forKey:key];
    });
}

@end

互斥锁

互斥锁为什么安全

因为互斥锁出现优先级反转后,高优先级的任务不会忙等。因为处于等待状态的高优先级任务,没有占用时间片,所以低优先级任务一般都能进行下去,从而释放掉锁。

互斥锁性能

1. @synchronized

@synchronized的使用非常简单,代码如下,传入一个想要加锁的对象,在其中执行加锁的相关逻辑即可。

@synchronized (obj) {}

那么其底层逻辑是如何实现的呢,我们可以看一下@synchronized的源码,通过打断点,查看其汇编源码,发现@synchronized就是实现了objc_sync_enterobjc_sync_exit两个方法,也就是说是通过这两个方法来实现加锁和解锁操作的。通过符号断点,我们可以知道其代码在objc源码中。

首先注意enterexit中都首先对obj是否为nil做了判断,如果obj为空时,则不会进行加锁和解锁的相关操作。所以在使用时一定要注意传入的值会不会被析构,造成传入值为空的情况,从而加锁失败

比如在线程异步同时操作同一个对象时,因为递归锁会不停的alloc/release,这时候某一个对象会可能是nil,从而导致加锁失败

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing// 如果obj为空,则不进行加锁操作
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

-----------------------------------------------------------------------------------------------------------------------

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        ✅// 如果obj为空,则不进行解锁操作
        // @synchronized(nil) does nothing
    }
	

    return result;
}

在具体的实现逻辑中,我们可以看到通过id2data方法,对obj进行了捕获和释放的操作,并生成了一个SyncData类型的对象。我们发现SyncData是一个结构体,而且有一个SyncData类型的nextData变量,指向下个数据,所以我们可以知道SyncData是一个链表结构中的一个元素。所以这是一个递归锁。

  • nextData指的是链表中下一个元素
  • object指的是传入需要加解锁的对象
  • threadCount就表示当前的线程数量
  • mutex即对象所关联的锁
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

了解了SyncData结构后,我们继续来查看源码,由于源码比较长,所以我们分模块俩讲解。

1.1 准备SyncData

我们可以看到会会通过LOCK_FOR_OBJLIST_FOR_OBJ取出object所对应的lockplistp

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    ...
}

既然我们在任何地方都可以直接通过调用方法来使用,那么说明底层必然维护着一套内部的存储。通过代码我们也可以看出,系统在底层维护了一个哈希表,里面存储了SyncList结构的数据,而SyncList是一个结构体,包含一个SyncData的头结点和一个spinlock_t锁对象

-----------------------------------------------------------------------------------------------------------------------
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
-----------------------------------------------------------------------------------------------------------------------
// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

static StripedMap<SyncList> sDataLists;

1.2 快速检查线程缓存

此步操作会通过tls封装的相关pthead操作线程的相关增删改查方法,获取到单个线程中缓存的SyncData数据,并进行快速查询和缓存

static SyncData* id2data(id object, enum usage why)
{
    ...
    #if SUPPORT_DIRECT_THREAD_KEYS
    // Check per-thread single-entry fast cache for matching object// 检查每线程单项快速缓存中是否有匹配的对象
    bool fastCacheOccupied = NO;
        ✅// 通过tls相关封装的pthead方法获取是否有再底层存储的SyncData
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;
        ✅// 如果获取到的数据和传入数据相同
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                // 如果是 entry,则对 lockCount 加 1,并通过 tls 保存
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                // 如果是 exit,则对 lockCount 减 1,并通过 tls 保存
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    // 如果 lockCount 为 0,则从高速缓存中删除
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif
    ...
}

1.3 检查有锁线程中的缓存

这步操作是检查所有线程中的缓存

static SyncData* id2data(id object, enum usage why)
{
    ...
    // Check per-thread cache of already-owned locks for matching object
    // 检查已拥有锁的每个线程高速缓存中是否有匹配的对象
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                item->lockCount++;
                break;
            case RELEASE:
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
    ...
}

1.4 全局哈希表查找

如果上述两步中,单个线程和已经锁住的线程中的缓存数据都没有找到的话,那么就会来到此步,回来系统保存的哈希表中SyncList结果中,进行链式查找。

static SyncData* id2data(id object, enum usage why)
{
    ...
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }
    ...
}

1.5 生成新数据并写入缓存

static SyncData* id2data(id object, enum usage why)
{
    ...
    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();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.// 只有创建的 SyncData 才能进入这里。// 所有的释放、检查和递归获取都是由上面的线程缓存处理
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {
            // Save in fast thread cache// 存入快速线程缓存
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache// 存入线程缓存
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}

至此一个@synchronized的相关操作已经执行完成。总结来说就是底层保存了一个哈希表,其中存储了SyncData结构的一个链表,通过线程缓存等操作,来进行增删改查,从来实现加解锁。但是操作结构复杂,步骤多,导致性能较滴,而且需要注意传入的obj不能为空,否则无法进行锁操作。

2. dispatch_semaphore

相关信号量的底层原理,再上一章节已经讲过,可以直接查看☞iOS底层学习 - 多线程之GCD底层原理篇

3. NSLock

NSLock的使用也非常的简单,只需要再需要进行加锁逻辑的前后,加上[_lock lock][_lock unlock]两行代码,就可以实现加锁的逻辑。

在寻找源码中,我们发现NSLock源码在CoreFundation框架中,无法进行查看,所以我们看Swift版本的CoreFundation实现,来类比NSLock实现,应该也是差不多的。通过源码我们可以发现

  • NSLock就是对pthread_mutex互斥锁的一种上层封装。
  • 是一种互斥锁,但不是递归锁
open class NSLock: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif

    public override init() {
        pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
#endif
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    open func unlock() {
        pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
#endif
    }

既然NSLock不是递归锁,那么他就存在着一个坑点:当我们对同一个线程,加锁两次的话,就会造成一直阻塞,就比如下面的代码,多线程调用时,会造成lock多次,从而无法向下进行。这个时候可以使用递归锁来解决。

    NSLock *testlock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [testlock lock];
            if (value > 0) {
                NSLog(@"current value = %d",value);
                // 异步递归调用
                testMethod(value - 1);
            }
            [testlock unlock];
        };
        testMethod(10);
    });

3. NSRecursiveLock

将上面例子中的NSLock换成NSRecursiveLock就是递归锁的使用了,和NSLock是类似的,并且能够解决NSLock在多线程中多次加锁的问题。

首先我们还是来看一下源码实现,发现NSRecursiveLock也是对pthread_mutex的封装,但是初始化的时候添加了PTHREAD_MUTEX_RECURSIVE递归相关的操作。

open class NSRecursiveLock: NSObject, NSLocking {
    internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1)
    private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
    private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
    
    withUnsafeMutablePointer(to: &attrib) { attrs in
            pthread_mutexattr_init(attrs)
            pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
            pthread_mutex_init(mutex, attrs)
        }
        
    pthread_cond_init(timeoutCond, nil)
    pthread_mutex_init(timeoutMutex, nil)

    public override init() {
        super.init()
        var attrib = pthread_mutexattr_t()
        pthread_cond_init(timeoutCond, nil)
        pthread_mutex_init(timeoutMutex, nil)
    }
    
    deinit {
        pthread_mutex_destroy(mutex)
        mutex.deinitialize(count: 1)
        mutex.deallocate()
        deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    
    open func unlock() {
        pthread_mutex_unlock(mutex)
        // Wakeup any threads waiting in lock(before:)
        pthread_mutex_lock(timeoutMutex)
        pthread_cond_broadcast(timeoutCond)
        pthread_mutex_unlock(timeoutMutex)
    }
    
    open func `try`() -> Bool {
        return pthread_mutex_trylock(mutex) == 0
    }
    
    open func lock(before limit: Date) -> Bool {
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
    }
    open var name: String?
}

我们都知道,使用递归的时候,最主要的是要有一个出口,否则非常容易形成死锁。比如刚才的代码,如果进行for循环创建多线程时。这时候就是造成死锁崩溃。

因为这个时候for循环造成多线程的多次创建,开辟了多条线程,但是NSRecursiveLock对象只有一个,线程之间同一个锁的对象状态是不能共享的,所以造成了线程1进行lock后,未执行到unlock时,线程2就进行了lock,所以造成了线程 1 等线程 2 解锁,线程 2 等线程 1 解锁的死锁状况。

那么这种情况下,使用哪种方案比较好呢?

这个时候使用@synchronized可以完美解决问题,因为@synchronized锁的是同一个对象,下次线程来进行锁操作时,会先从缓存中进行查找,不会进行多次锁,所以是安全的。

NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [recursiveLock lock];
            if (value > 0) {
                NSLog(@"current value = %d",value);
                testMethod(value - 1);
            }
            [recursiveLock unlock];
        };
        testMethod(10);
    });
}
常用锁总结:当只是普通线程安全的时候,使用 NSLock就可以解决,而需要保证递归调用线程安全的时候,使用 NSRecursiveLock,而又需要循环,外界的线程也会造成影响的时候,为了解决死锁的问题,我们可以使用@synchronized来解决

4. NSCondition

NSCondition是一个条件锁。

在线程间的同步中,有这样一种情况: 线程 A 需要等条件 C 成立,才能继续往下执行.现在这个条件不成立,线程 A 就阻塞等待. 而线程 B 在执行过程中,使条件 C 成立了,就唤醒线程 A 继续执行。这个时候,我们可以使用条件锁来完成相关逻辑。

条件锁的底层实现其实就是一个互斥锁和条件变量的封装,由于未开源,我们还是先看Swift源码。

  • NSCondition是对mutexcond的一种封装。cond就是用于访问和操作特定类型数据的指针
  • wait操作在没有超时时,会阻塞线程,使其进入休眠状态,需要在lock状态下使用
  • signal操作是唤醒一个正在休眠等待的线程,需要在lock状态下使用
  • broadcast唤醒所有正在等待的线程,需要在lock状态下使用
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)
    }
    
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    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
    }

}

对于条件锁,我们经常用来解决的就是生产者-消费者模式的相关问题。比如数组中的元素,只有在大于0的情况下,才可以进行删除操作,这种情况下,可以考虑使用条件锁。

_condition = [[NSCondition alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self consumer];
});

- (void)producer{
    [_condition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_condition signal];
    [_condition unlock];

}

- (void)consumer{
    
    // 线程安全
    [_condition lock];
    ✅// 使用while因为NSCondition可以给每个线程分别加锁,但加锁后不影响其他线程进入临界区。// 所以 NSCondition使用 wait并加锁后,并不能真正保证线程的安全。// 当一个signal操作发出时,如果有两个线程都在做消费者操作,那同时都会消耗掉资源,于是绕过了检查。

    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保证正常流程
        [_condition wait];
    }
    
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    [_condition unlock];
}

5. NSConditionLock

NSConditionLock。我们可以通过Swift源码查看可得

  • NSConditionLockNSCondition加线程数的封装,继承NSLocking协议,也有lockunlock等方法
  • 实现了类似dispatch_semaphore的效果
open class NSConditionLock : NSObject, NSLocking {
    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
    }

    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
    }

    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)
    }

    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
    }
    
    open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
        // 使用 NSCondition 加锁
        _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?
}

具体的用法可以参考下面的代码

// 初始化 NSConditionLock,并设置 condition 的值为 2
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    // 需要等到 condition 为 1 的时候执行下面的代码
    [conditionLock lockWhenCondition:1];
    NSLog(@"线程 1");
    [conditionLock unlockWithCondition:0];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    // 因为 condition 为 2,所以执行下面的代码
    [conditionLock lockWhenCondition:2];
    NSLog(@"线程 2");
    // 解锁,并将 condition 设置为 1
    [conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 因为没有条件限制,所以可以直接执行下面的代码
    [conditionLock lock];
    NSLog(@"线程 3");
    [conditionLock unlock];
});
-----------------------------------------------------------------------------------------------------------------------
// 打印结果
线程 3
线程 2
线程 1

6. os_unfair_lock

由于OSSpinLock自旋锁的bug,在iOS10之后OSSpinLock被废弃,内部封装了os_unfair_lock,而os_unfair_lock在加锁时会处于休眠状态,而不是自旋锁的忙等状态。

总结

  • OSSpinLock之所以不在安全,是因为自旋锁会在线程等待时处于忙等状态,会造成任务优先级翻转,倒是无法执行,目前用os_unfair_lock来替代,是一个互斥锁,互斥锁不会处于忙等,不占用时间片。
  • atomic底层实现原理就是对getset方法进行加锁,但是不能保证多条线程调用或者不适用getset的线程安全,且性能消耗巨大
  • 读写锁实际是⼀种特殊的⾃旋锁,只允许一个写者写入,但是可以有多个读者。可以使用并发队列+dispatch_barrier_async的方法,来实现一个类似的读写锁
  • @synchronized要注意传入的对象不能为nil,否则无法加锁。底层逻辑是维护了一个全局的哈希表用来存储对象和锁,会按照缓存线程->所有线程->全局哈希表的方式进行增删改查
  • NSLock是对pthread_mutex的封装,但是没有递归逻辑。对同一个线程多次lock会造成阻塞。NSRecursiveLock是在NSLock的基础上添加了递归逻辑,当只有一个递归锁对象,多线程进行锁操作时,会造成死锁,可用@synchronized解决
  • NSConditionNSConditionLock是条件锁,当满足某一个条件时,才能进行操作,适用于生产者消费者模式,和信号量dispatch_semaphore类似

参考资料

iOS 的锁

iOS 锁的底层探索笔记