iOS中锁的使用及其原理

1、锁的归类

锁的分类只有两大类自旋锁和和互斥锁。这两大类下又分成很多不同的小类。了解锁之前建议先了解一下线程及线程安全
自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取自旋锁,线程会一直保持该锁,直至显式释放自旋锁。自旋锁避免了线程上下文切换的调度开销,因此对于线程只会阻塞很短的时间是很高效的,但是对于比较长时间的阻塞也是比较消耗CPU的。iOS常见的自旋锁:

  • OSSpinLock

互斥锁:是一种用于多线程编程中,防止两条线程同时对一公共资源(比如全局变量)进行读写的机制。它是通过将代码切成一个一个的临界区而达成的。iOS中常见的互斥锁有:

  • NSLock
  • pthread_mutex
  • @synchronized

条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。iOS中常见的条件锁:

  • NSCondition
  • NSConditionLock

递归锁: 就是一个线程可以加锁N次而不会引发死锁。iOS常见的递归锁:

  • NSRecursiveLock
  • pthread_mutex(recursive)

信号量(semaphore):是一种更高级别的同步机制,互斥锁可以说是semaphore在取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。 iOS的信号量:

  • dispatch_semaphore

读写锁: 读写锁实际上是一种特殊的自旋锁,它把对共享资源的访问划分为读者和写者,读者只能对共享资源进行访问,写者则可以对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或者多个读者,但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得资源,否则必须自选在哪里,直到没有任何写者或者读者。如果读写锁当前没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。正是因为这个原因,当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会阻塞;当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权限,但是如果线程希望以写模式对此锁进行加锁,它必须等到所有线程释放锁。通常,当读写锁处于读模式时,如果另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式长期占用而导致等待的写模式线程长期阻塞。
读写锁适合用于对数据结构的读次数比写次数多得多的情况。因为,读模式锁定时可以共享,以写模式锁住时意味着独占,所以读写锁又叫共享-独占锁。iOS中的读写锁:

  • pthread_rwlock_rlock
  • pthread_rwlock_wlock

2、iOS中常见锁的原理及使用

2.1、@synchronized
使用@synchronized同步代码示例:

    NSObject *obj = [[NSObject alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        @synchronized(obj){
            NSLog(@"线程1 start");
            sleep(2);
            NSLog(@"线程1 end");
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        sleep(1);
        @synchronized(obj){
            NSLog(@"线程2");
        }
    });

这段代码的打印结果是:

21-07-19 15:53:49.053015+0800 iOSLockTestDemo[15604:9853037] 线程1 start
2021-07-19 15:53:51.054994+0800 iOSLockTestDemo[15604:9853037] 线程1 end
2021-07-19 15:53:51.055214+0800 iOSLockTestDemo[15604:9853030] 线程2

@synchronized(obj)指令使用的obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj)改为@synchronized(self),刚线程2就不会被阻塞,代码示例如下:

  NSObject *obj = [[NSObject alloc] init];
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        @synchronized(obj){
            NSLog(@"线程1 start");
            sleep(2);
            NSLog(@"线程1 end");
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        sleep(1);
        @synchronized(self){
            NSLog(@"线程3");
        }
    });

以上代码打印结果:

2021-07-19 15:55:20.861569+0800 iOSLockTestDemo[15631:9855072] 线程1 start
2021-07-19 15:55:21.865265+0800 iOSLockTestDemo[15631:9855076] 线程3
2021-07-19 15:55:22.865633+0800 iOSLockTestDemo[15631:9855072] 线程1 end

@synchronized还是个递归可重入锁,如下代码所示:

NSObject *obj = [[NSObject alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        @synchronized(obj){
            NSLog(@"线程开始");
            @synchronized (obj) {
                NSLog(@"线程4重入1次开始");
                @synchronized (obj) {
                    NSLog(@"线程4重入2次");
                }
                NSLog(@"线程4重入1次完成");
            }
            NSLog(@"线程结束");
        }
    });

上述代码打印结果:

2021-07-19 17:08:29.120607+0800 iOSLockTestDemo[17806:1063480] 线程开始
2021-07-19 17:08:29.120912+0800 iOSLockTestDemo[17806:1063480] 线程4重入1次开始
2021-07-19 17:08:29.121111+0800 iOSLockTestDemo[17806:1063480] 线程4重入2次
2021-07-19 17:08:29.121285+0800 iOSLockTestDemo[17806:1063480] 线程4重入1次完成
2021-07-19 17:08:29.121435+0800 iOSLockTestDemo[17806:1063480] 线程结束

@synchronized是个递归互斥锁,同一个线程可以重复获得这个锁并进入执行执行块里面的代码而不会导致死锁。
@synchronized的优点:

  • 不需要在代码中显式的创建锁对象,便可以实现锁的机制;
  • 递归互斥,同一个线程可以重复进入而不导致死锁。

@synchronized的缺点:

  • 效率低。@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁,这会增加额外的开销。同时为了实现递归互斥可重入,底层使用的是递归锁加上复杂的业务逻辑,也增加了不少的消耗。
  • @synchronized加锁需要一个token(demo中的obj),在选着token的时候要特别注意不能让token为nil,否则加锁无效。

@synchronized底层原理
接下来的问题是@synchronized底层是如何实现递归互斥的?是如何实现可重入的呢?其实主要原因是它的底层使用了递归锁,可重入的原因是底层使用了一个计数器,用来记录锁的次数。
接下来我们通过源码来看个究竟。@synchronized的底层实现有两个关键函objc_sync_enter和objc_sync_exit:

  • objc_sync_enter
    获得锁成功之后就会调用这个函数。它的源码如下:
// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
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
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }
    return result;
}

通过源码可以看出,当obj为nil时会调用objc_sync_nil()函数,这个函数是直接返回的的,没有加锁操作,所以这个obj不能为nil。当obj不为空的时候,通过id2data函数去获取一个SyncData类型的数据结构,然后这个结构有个属性mutex,它是一个递归锁,@synchronized加锁就是通过这个mutex.lock()实现的。实际上id2data这个函数实现了很多核心逻辑,其源码如下:

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 //tls-本地局部的线程缓存吧
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    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: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    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

    // 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;
        }
    }

    // Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    
    lockp->lock();

    {
        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;
        }
    }

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    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.
        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;
}

这个源码里涉及到几个数据结构SyncData、SyncList、SyncCache、
SyncCacheItem和StripedMap<SyncList> sDataLists。他们的定义如下:

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;

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

/*
  Fast cache: two fixed pthread keys store a single SyncCacheItem. 
  This avoids malloc of the SyncCache for threads that only synchronize 
  a single object at a time.
  SYNC_DATA_DIRECT_KEY  == SyncCacheItem.data
  SYNC_COUNT_DIRECT_KEY == SyncCacheItem.lockCount
 */

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;

StripedMap<SyncList> sDataLists 是一个哈希结构,SyncList是SyncData链表的表头,它的结构大致如下:


SyncDataMap.jpg

结合源码可以看出,当obj不为空时,@synchronized的工作流大致如下:

  • 1、当一个线程第一次进来的时候,这时没有锁、没有缓存。这时候会创建一个新的SyncData,然后关联哈希表sDataLists,代码如下:
    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    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;

创建完成之后写入缓存然后返回,源码:

#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++;
        }

这时候缓存SyncCache中的对应的SyncCacheItem的lockCount=1,SyncData中的threadCount=1;

  • 2、当同一个线程再次(多次)进来的时候,lockCount++, 缓存;

  • 3、当不同的线程进来的时候,这时候threadCount++, lockCount++。
    lockCount是保障了单线程可重入而不死锁,而threadCount则保障了不会因为多线程相互等待而导致的死锁。

  • objc_sync_exit
    在代码块执行结束的时候会调用这个函数进行unlock。同时lockCount--,当lockCount <= 0时,threadCount--。
    通过对threadCount和lockCount的维护来对锁进行管理,以实现可重入的目的。

2.2、NSLock
NSLock使用很简单,它是Cocoa提供给我们最基本的锁对象,底层是通过pthread_mutex实现的,这也是我们经常所使用的,代码如下:

    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            self.testArray = [NSMutableArray array];
            [lock unlock];
        });
    }

上面的代码如果把lock去掉就会崩溃。除lock和unlock方法外,NSLock还提供了tryLock和lockBeforeDate:两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。代码演示:

 NSLock * lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        [lock lockBeforeDate:[NSDate date]];
        NSLog(@"线程1 开始");
        sleep(2);
        NSLog(@"线程1 结束");
        [lock unlock];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        sleep(1);
        if([lock tryLock]) {//尝试获取锁,如果获取不到返回NO,不会阻塞该线程NSLog(@"锁可用的操作");
            [lock unlock];
        }else{
            NSLog(@"锁不可用");
        }
        NSDate*date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
        if([lock lockBeforeDate:date]) {//尝试在未来的3s内获取锁,并阻塞该线程,如果3s内获取不到恢复线程, 返回NO,不会阻塞该线程
            NSLog(@"没有超时,能获得锁");
            [lock unlock];
        }else{
            NSLog(@"超时,无法获得锁");
        }
    });

上面代码的执行结果为:

2021-07-20 10:45:13.323313+0800 iOSLockTestDemo[19123:10215889] 线程1 开始
2021-07-20 10:45:14.323755+0800 iOSLockTestDemo[19123:10215890] 锁不可用
2021-07-20 10:45:15.328511+0800 iOSLockTestDemo[19123:10215889] 线程1 结束
2021-07-20 10:45:15.328805+0800 iOSLockTestDemo[19123:10215890] 没有超时,能获得锁

2.3、NSRecursiveLock
NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。
NSRecursiveLock的使用我们可以与NSLock进行对比来更好的了解,比如如下代码用NSLock实现:

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

这段代码是一个典型的死锁情况,testMethod在当前线程是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。调试器中会输出如下信息:

2021-07-20 10:53:23.986601+0800 iOSLockTestDemo[19243:10225964] current value = 10

在这种情况下,如果替换成NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。如果我们将NSLock代替为NSRecursiveLock,上面代码则会正确执行。代码如下:

 NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [lock lock];
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
            [lock unlock];
        };
        testMethod(10);
    });

执行结果如下:

2021-07-20 10:59:49.184600+0800 iOSLockTestDemo[19296:10230483] current value = 10
2021-07-20 10:59:49.188215+0800 iOSLockTestDemo[19296:10230483] current value = 9
2021-07-20 10:59:49.192839+0800 iOSLockTestDemo[19296:10230483] current value = 8
2021-07-20 10:59:49.196742+0800 iOSLockTestDemo[19296:10230483] current value = 7
2021-07-20 10:59:49.198144+0800 iOSLockTestDemo[19296:10230483] current value = 6
2021-07-20 10:59:49.199597+0800 iOSLockTestDemo[19296:10230483] current value = 5
2021-07-20 10:59:49.200831+0800 iOSLockTestDemo[19296:10230483] current value = 4
2021-07-20 10:59:49.201976+0800 iOSLockTestDemo[19296:10230483] current value = 3
2021-07-20 10:59:49.202657+0800 iOSLockTestDemo[19296:10230483] current value = 2
2021-07-20 10:59:49.206417+0800 iOSLockTestDemo[19296:10230483] current value = 1

像这种递归调用其实也可以用@synchronized来代替的,而且大部分情况下性能差不多。@synchronized相对来说适用性更广。
2.4、NSCondition
NSCondition一种最基本的条件锁。手动控制线程wait和signal。NSCondition类似于生产者-消费者模型,生产和消费同时进行,但是只有生产出足够消费的产品才能开始消费。代码示例:

    NSCondition *condition = [[NSCondition alloc] init];
    __block int i = 0;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        while (1) {
            [condition lock];
            if (i == 0) {
                NSLog(@"线程1等待");
                [condition wait];
            }
            i = 0;
            NSLog(@"线程1继续执行");
            [condition unlock];
        }
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        while (1) {
            [condition lock];
            i = 1;
            NSLog(@"线程2告诉线程1不用等待");
            [condition signal];
            [condition unlock];
            sleep(1);
        }
    });

上面代码执行结果如下:

2021-07-20 12:45:55.868665+0800 iOSLockTestDemo[20075:10303489] 线程1等待
2021-07-20 12:45:55.868964+0800 iOSLockTestDemo[20075:10303485] 线程2告诉线程1不用等待
2021-07-20 12:45:55.869151+0800 iOSLockTestDemo[20075:10303489] 线程1继续执行
......

[condition lock];一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要lock 外等待,只到unlock ,才可访问[condition unlock];与lock 同时使用[condition wait]; 让当前线程处于等待状态[condition signal]; CPU发信号告诉线程不用在等待,可以继续执行。
2.5、NSConditionLock
当我们在使用多线程的时候,有时一把只会lock和unlock的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数情况是只有满足一定条件的情况下才能打开这把锁,这时候就需要NSConditionLock(条件锁),代码示例:

    NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [conditionLock lockWhenCondition:1]; 
        NSLog(@"线程 1");
         [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
        [conditionLock lockWhenCondition:2];
        NSLog(@"线程 2");
        [conditionLock unlockWithCondition:1];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlockWithCondition:2];
    });

上面代码执行结果如下:

2021-07-20 11:17:32.830479+0800 iOSLockTestDemo[19442:10244097] 线程 3
2021-07-20 11:17:32.848170+0800 iOSLockTestDemo[19442:10244097] 线程 2
2021-07-20 11:17:32.849217+0800 iOSLockTestDemo[19442:10243439] 线程 1

在线程3中的加锁使用了lock,所以是不需要条件的,所以顺利的就锁住了,而线程2则需要一把被标识为2的钥匙, 所以在线程3结束调用[conditionLock unlockWithCondition:2]发送标识2的钥匙,才最终打开了线程2中的阻塞。同理,线程1也要等到线程2结束发送标识为1的钥匙才能继续执行。
NSConditionLock实际是NSCondition的进一步封装,也跟其它的锁一样,是需要lock与unlock对应的,只是lock,lockWhenCondition:与unlock,unlockWithCondition:是可以随意组合的,当然这是与你的需求相关的。但在unlock的使用了一个整型的条件,它可以开启其它线程中正在等待这把钥匙的临界地。
2.6、dispatch_semaphore
dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。

(1)dispatch_semaphore_create的声明为:

dispatch_semaphore_t dispatch_semaphore_create(long value);

传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。

值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

(2)dispatch_semaphore_signal的声明为:

long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

这个函数会使传入的信号量dsema的值加1;

(3) dispatch_semaphore_wait的声明为:

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

这个函数会使传入的信号量dsema的值减1;这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。代码示例:

    dispatch_semaphore_t signal =dispatch_semaphore_create(1);
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW,3* NSEC_PER_SEC);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
      dispatch_semaphore_wait(signal,overTime);
        NSLog(@"线程1开始");
        sleep(2);
        NSLog(@"线程1结束");
        dispatch_semaphore_signal(signal);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        sleep(1);
        dispatch_semaphore_wait(signal,overTime);
        NSLog(@"线程2");
        dispatch_semaphore_signal(signal);
    });

上面代码的执行结果为:

2021-07-20 12:50:57.189124+0800 iOSLockTestDemo[20107:10306945] 线程1开始
2021-07-20 12:50:59.193231+0800 iOSLockTestDemo[20107:10306945] 线程1结束
2021-07-20 12:50:59.193770+0800 iOSLockTestDemo[20107:10306940] 线程2

dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。

如上的代码,如果超时时间overTime设置成>2,可完成同步操作。如果overTime<2的话,在线程1还没有执行完成的情况下,此时超时了,将自动执行下面的代码。

2.7、pthread_mutex
c语言定义下多线程加锁方式。
1:pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t attr);初始化锁变量mutex。attr为锁属性,NULL值为默认属性。
2:pthread_mutex_lock(
pthread_mutex_tmutex);加锁
3:pthread_mutex_tylock(
pthread_mutex_tmutex);加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。
4:pthread_mutex_unlock(
pthread_mutex_t
mutex);释放锁
5:pthread_mutex_destroy(
pthread_mutex_t***mutex);使用完后释放

    __block pthread_mutex_t theLock;
    pthread_mutex_init(&theLock,NULL);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        pthread_mutex_lock(&theLock);
        NSLog(@"线程1开始");
        sleep(3);
        NSLog(@"线程1结束");
        pthread_mutex_unlock(&theLock);
        
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        sleep(1);pthread_mutex_lock(&theLock);
        NSLog(@"线程2");
        pthread_mutex_unlock(&theLock);
    });
}

代码执行操作结果如下:

2021-07-20 12:54:33.441769+0800 iOSLockTestDemo[20129:10310609] 线程1开始
2021-07-20 12:54:36.443789+0800 iOSLockTestDemo[20129:10310609] 线程1结束
2021-07-20 12:54:36.444066+0800 iOSLockTestDemo[20129:10309372] 线程2

2.8、pthread_mutex(recursive)
这是pthread_mutex为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。

    __block pthread_mutex_t lock;
//    pthread_mutex_init(&lock, NULL);//生成普通互斥锁
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);//生成递归互斥锁
    pthread_mutex_init(&lock,&attr);
    pthread_mutexattr_destroy(&attr);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        static void (^RecursiveMethod)(int);
        RecursiveMethod = ^(int value) {
            pthread_mutex_lock(&lock);
            if (value>0) {
                NSLog(@"value = %d", value);
                sleep(1);
                RecursiveMethod(value-1);
            }
            pthread_mutex_unlock(&lock);
        };
        RecursiveMethod(5);
    });

如果使用pthread_mutex_init(&lock, NULL);初始化锁的话,上面的代码会出现死锁现象。如果使用递归锁的形式,则没有问题。

2.9、OSSpinLock& os_unfair_lock
OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。 不过据说会导致优先级反转导致死锁的问题。所谓优先级反转,简单说就是如果一条低优先级的线程获取了当前这个锁,还在执行任务,而这时候有条高优先级线程过来获取当前这个锁的时候由于锁已被占用,而高优先级线程会一直自旋,占用CPU,导致持有锁的线程无法获取CPU资源执行后面的代码释放锁,这样就形成了一个相互等待的死循环。OSSpinLock代码示例:

_spinLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        OSSpinLockLock(&_spinLock);
        NSLog(@"线程1开始");
        sleep(2);
        NSLog(@"线程1完成");
        OSSpinLockUnlock(&_spinLock);
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        OSSpinLockLock(&_spinLock);
        NSLog(@"线程2开始");
        sleep(2);
        NSLog(@"线程2完成")
        OSSpinLockUnlock(&_spinLock);
    });

上面的代码执行结果:

2021-07-20 13:06:51.733227+0800 iOSLockTestDemo[20316:10322092] 线程2开始
2021-07-20 13:06:54.740453+0800 iOSLockTestDemo[20316:10322092] 线程2完成
2021-07-20 13:06:54.783666+0800 iOSLockTestDemo[20316:10322090] 线程1开始
2021-07-20 13:06:57.788149+0800 iOSLockTestDemo[20316:10322090] 线程1完成

由于会导致优先级反转的问题,所以苹果已经不推荐使用,苹果底层使用OSSpinLock都已经替换成了os_unfair_lock了。os_unfair_lock的使用代码示例:

    os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT;
    os_unfair_lock_lock(&unfairLock);
    // doSomething
    os_unfair_lock_unlock(&unfairLock);

性能对比

上面列举的是iOS中常用的锁,他们的实现机制各不相同,性能也各不一样。下面引用来源于网络的一张图片,图中是对各种锁进行相同次的多次操作之后得出的结果,它们多次操作的性能对比如下:


图片来自网络

总的来说:
OSSpinLock和dispatch_semaphore的效率远远高于其他。@synchronized和NSConditionLock效率较差。鉴于OSSpinLock的不安全,所以我们在开发中如果考虑性能的话,建议使用dispatch_semaphore。如果不考虑性能,只是图个方便的话,那就使用@synchronized。

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

推荐阅读更多精彩内容