[iOS] 线程锁 — synchronized & 各种Lock

1. 为什么多线程需要锁?

首先在多线程处理的时候我们经常会需要保证同步,这是为啥呢,看一下下面这个例子:

NSInteger count;
count = 50;

- (void)test {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self lockSection];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self lockSection];
        }
    });
}

- (void)lockSection {
    NSLog(@"before count: %ld", (long)count);
    count = count - 1;
    NSLog(@"after count: %ld", (long)count);
}

这种时候我们期待的输出大概就是按顺序,50、49、48……这种,但是实际上嘞:

2019-11-04 14:57:22.584024+0800 [14572:162956] before count: 50
2019-11-04 14:57:22.584022+0800 [14572:162947] before count: 50
2019-11-04 14:57:22.584123+0800 [14572:162956] after count: 49
2019-11-04 14:57:22.584186+0800 [14572:162956] before count: 49
2019-11-04 14:57:22.584206+0800 [14572:162947] after count: 48
2019-11-04 14:57:22.584254+0800 [14572:162956] after count: 47
2019-11-04 14:57:22.584270+0800 [14572:162947] before count: 47
2019-11-04 14:57:22.584323+0800 [14572:162956] before count: 47
2019-11-04 14:57:22.584535+0800 [14572:162947] after count: 46
2019-11-04 14:57:22.584621+0800 [14572:162956] after count: 45
2019-11-04 14:57:22.584934+0800 [14572:162956] before count: 45
2019-11-04 14:57:22.585000+0800 [14572:162947] before count: 45
2019-11-04 14:57:22.585289+0800 [14572:162956] after count: 44
2019-11-04 14:57:22.585544+0800 [14572:162956] before count: 43
2019-11-04 14:57:22.585499+0800 [14572:162947] after count: 43
2019-11-04 14:57:22.585783+0800 [14572:162956] after count: 42
2019-11-04 14:57:22.586086+0800 [14572:162956] before count: 42
2019-11-04 14:57:22.586100+0800 [14572:162947] before count: 42
2019-11-04 14:57:22.586301+0800 [14572:162956] after count: 41
2019-11-04 14:57:22.586618+0800 [14572:162947] after count: 40
2019-11-04 14:57:22.586651+0800 [14572:162956] before count: 40
2019-11-04 14:57:22.587034+0800 [14572:162956] after count: 39
2019-11-04 14:57:22.586989+0800 [14572:162947] before count: 40
……

感受一下这个bug,为啥已经count=50的时候进入了但是下次进入又是50呢,其实这个就是线程的问题了,系统在运行的时候每个线程都是干一会儿活就会让出时间片,让其他线程再干一会儿,交替执行。

于是可能线程1刚进入count=50的时候,下一步count=count-1还没执行就让出了时间片,于是进程2就进入了,这个时候由于count仍旧是50,所以打印的就会重复啦。

所以如果我们想保证不要出现多线程的冲突问题,就需要线程同步了。其实xcode提供检测多线程操作同一数据的工具非常方便,通过edit schema就可以啦:

edit schema

勾选这个选项,再次运行程序,会发现它在出现多线程访问同一数据并且对它进行操作的时候断点:


data race

2. 如何进行多线程同步

主要还是靠加锁。。iOS有很多锁,包括:NSLock、semaphore、OSSpinLock神马的。根据参考文章里面的test,他们加解锁的时间大概如下:


性能
2.1 @synchronized

先来看加锁最慢的一个,也是在Android中蛮常用到的一个~

如果改写为:

- (void)lockSection {
    @synchronized (self) {
        NSLog(@"before count: %ld", (long)count);
        count = count - 1;
        NSLog(@"after count: %ld", (long)count);
    }
}

输出就变为了:

2019-11-18 16:29:58.503712+0800 Example1[45488:373901] before count: 50
2019-11-18 16:29:58.503825+0800 Example1[45488:373901] after count: 49
2019-11-18 16:29:58.503907+0800 Example1[45488:373901] before count: 49
2019-11-18 16:29:58.503986+0800 Example1[45488:373901] after count: 48
2019-11-18 16:29:58.504053+0800 Example1[45488:373901] before count: 48
2019-11-18 16:29:58.504115+0800 Example1[45488:373901] after count: 47
2019-11-18 16:29:58.504244+0800 Example1[45488:373901] before count: 47
2019-11-18 16:29:58.504304+0800 Example1[45488:373901] after count: 46
2019-11-18 16:29:58.504370+0800 Example1[45488:373901] before count: 46
2019-11-18 16:29:58.504534+0800 Example1[45488:373901] after count: 45
2019-11-18 16:29:58.504734+0800 Example1[45488:373901] before count: 45
2019-11-18 16:29:58.504951+0800 Example1[45488:373901] after count: 44

据说 @synchronized block 会变成 objc_sync_enter 和 objc_sync_exit 的成对儿调用。

@synchronized(obj) {
    //do work
}

转化成这样的东东:

@try {
    objc_sync_enter(obj);
    //do work
} @finally {
    objc_sync_exit(obj);    
}

查看objc-sync.h文件可以发现:

#ifndef __OBJC_SNYC_H_
#define __OBJC_SNYC_H_

#include <objc/objc.h>

/** 
 * Begin synchronizing on 'obj'.  
 * Allocates recursive pthread_mutex associated with 'obj' if needed.
 * 
 * @param obj The object to begin synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS once lock is acquired.  
 */
OBJC_EXPORT int
objc_sync_enter(id _Nonnull obj)
    OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);

/** 
 * End synchronizing on 'obj'. 
 * 
 * @param obj The object to end synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
 */
OBJC_EXPORT int
objc_sync_exit(id _Nonnull obj)
    OBJC_AVAILABLE(10.3, 2.0, 9.0, 1.0, 2.0);

enum {
    OBJC_SYNC_SUCCESS                 = 0,
    OBJC_SYNC_NOT_OWNING_THREAD_ERROR = -1
};


#endif // __OBJC_SYNC_H_

也就是说enter的时候其实是用pthread_mutex锁来实现了lock,根据源码https://opensource.apple.com/source/objc4/objc4-646/runtime/objc-sync.mm看一下到底做了些什么:

// 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);
        require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
    
        result = recursive_mutex_lock(&data->mutex);
        require_noerr_string(result, done, "mutex_lock failed");
    } 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();
    }

done: 
    return result;
}


// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR, "id2data failed");
        
        result = recursive_mutex_unlock(&data->mutex);
        require_noerr_string(result, done, "mutex_unlock failed");
    } else {
        // @synchronized(nil) does nothing
    }
    
done:
    if ( result == RECURSIVE_MUTEX_NOT_LOCKED )
         result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;

    return result;
}

也就是在enter和exit的时候都用了SyncData,然后通过recursive_mutex_lock和unlock操作data中的mutex。

那么SyncData是什么呢?

typedef struct SyncData {
    id object;
    recursive_mutex_t mutex;
    struct SyncData* nextData;
    int threadCount;
} SyncData;

typedef struct SyncList {
    SyncData *data;
    spinlock_t lock;
} SyncList;

// Use multiple parallel lists to decrease contention among unrelated objects.
#define COUNT 16
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data
static SyncList sDataLists[COUNT];

也就是说,有一个容量为16的SyncList数组,通过把obj的内存地址转为无符号整型右移5位然后和15的二进制表示做与运算,可以得到数组的下标。

拿到对应的SyncList以后,就可以拿到SyncData,然后通过data的mutex来上锁;spinlock_t是用来Spinlock prevents multiple threads from creating multiple locks for the same new object.

SyncData结构体包含:

  • 一个 object(嗯就是我们给 @synchronized 传入的那个对象)。
  • 一个有关联的 recursive_mutex_t,它就是那个跟 object 关联在一起的锁。
  • 一个指向另一个 SyncData 对象的指针,叫做 nextData,所以你可以把每个 SyncData 结构体看做是链表中的一个元素。
  • 一个 threadCount,这个 SyncData 对象中的锁会被一些线程使用或等待,threadCount 就是此时这些线程的数量。它很有用处,因为 SyncData 结构体会被缓存,threadCount==0 就暗示了这个 SyncData 实例可以被复用。

你可以把 SyncData 当做是链表中的节点。每个 SyncList 结构体都有个指向 SyncData 节点链表头部的指针,也有一个用于防止多个线程对此列表做并发修改的锁spinlock_t。

这里特别看下链表是啥:

// malloc a new SyncData and add to list.
// XXX calling malloc with a global lock held is bad practice,
// might be worth releasing the lock, mallocing, and searching again.
// But since we never free these guys we won't be stuck in malloc very often.
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = object;
result->threadCount = 1;
recursive_mutex_init(&result->mutex);
result->nextData = *listp;
*listp = result;

在找obj对应的SyncData的时候,会先通过obj内存地址转换成下标以后找的sync list,然后从list的sync data开始找,不断地next来找链表上是不是已经存在了obj对应的sync data。如果没有就创建一个SyncData,然后让这个新建的SyncData的next指向现在list的头data,并且将新建的data设为新的list头。

https://blog.csdn.net/TuGeLe/article/details/88399115里面的流程图总结的很好~


通过clang转为源码据说是酱紫的:

static void _I_CustomObject_testSynchronized(CustomObject * self, SEL _cmd) {
{
    id _rethrow = 0;
    id _sync_obj = (id)self;
    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);
        
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_p3_pyrv2p4j0gn_yqv6994w1ryr0000gn_T_CustomObject_77509d_mi_0);
    } catch (id e) {
        _rethrow = e;
        
    }
    
    { struct _FIN { _FIN(id reth) : rethrow(reth) {}
        ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
        id rethrow;
    } _fin_force_rethow(_rethrow);}
}

需要注意的是一开始就id _sync_obj = (id)self;保存了self也就是synchronized的object,即使之后这个object被置为了nil,由于_sync_obj还拿着引用计数,就不会被清掉内存。

但这个也说明了一个事情,就是被用于synchronized的只能是object。

Q: 那么为什么需要_sync_obj呢?

从 objc_sync_enter和 objc_sync_exit实现可知,当加锁条件为nil时,临界区代码正常执行,但无法加锁解锁,不能保证临界区代码在线程中的安全。

首先objc_sync_enter和objc_sync_exit如果传nil的话,其实代码里面什么都没做,只有obj不为空才会lock和unlock。

如果objc_sync_enter的时候obj不为空,lock了mutex锁,然后exit的时候传入了nil,那么这个mutex锁不会被unlock,就会导致死锁。

所以其实_sync_obj的作用就是keep住传入的object,确保它的引用计数不会为0,也就不会被清掉,所以lock和unlock是成对的。

Q: 既然@synchronized对加锁条件进行了强引用保护,那么是否可以在临界区代码中对加锁条件进行更改?

不建议在临界区代码中对加锁条件进行更改的操作。

原因在于若在临界区代码中对加锁条件进行更改,那么此时如果再次对该加锁条件进行加锁,此时获取的 SyncData为不同对象对应的值,虽说也能成功加锁,但是无法保证与第一次加锁线程互斥,可能造成业务逻辑的错误。

Q: 是否可以对所有需要锁的操作都使用同一个加锁条件?

不建议对所有需要锁的操作使用同一个加锁条件。

原因在于当某个操作对加锁条件进行加锁后,若其他与该操作无关的操作再对加锁条件进行加锁时,需等到前一个操作执行完毕,这可能造成无关操作多余无用的等待时间,造成程序效率低下。

所以建议对涉及共同资源的操作使用同一个加锁条件进行加锁,相互无关的操作使用不同的加锁条件加锁。


2.2 NSLock

NSLock 是 Objective-C 以对象的形式暴露给开发者的一种锁,它的实现非常简单,通过宏,定义了 lock 方法:

#define    MLOCK
- (void) lock {
  int err = pthread_mutex_lock(&_mutex);\
  // 错误处理 ……
}

NSLock 只是在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。

这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,仅仅是内部 pthread_mutex 互斥锁的类型不同。通过宏定义,可以简化方法的定义。

NSLock 比 pthread_mutex 略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。

Q: 插一个之前面试的时候一个帅气冷漠小哥哥问我的问题,如何用NSLock实现让任务A和B执行以后再执行C?

A: 后来问了另一个帅气不冷漠小哥哥,他提醒我可以用两把锁...感觉自己智商拙计了~


2.3 NSRecursiveLock

上文已经说过,递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。

NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE。


2.4 dispatch_semaphore信号量

dispatch_semaphore_t 最终会调用到 sem_wait 方法,这个方法在 glibc 中被实现如下:

int sem_wait (sem_t *sem) {
  int *futex = (int *) sem;
  if (atomic_decrement_if_positive (futex) > 0)
    return 0;
  int err = lll_futex_wait (futex, 0);
    return -1;
}

首先会把信号量的值减一,并判断是否大于零。如果大于零,说明不用等待,所以立刻返回。具体的等待操作在 lll_futex_wait 函数中实现,lll 是 low level lock 的简称。这个函数通过汇编代码实现,调用到 SYS_futex 这个系统调用,使线程进入睡眠状态,主动让出时间片,这个函数在互斥锁的实现中,也有可能被用到。

主动让出时间片并不总是代表效率高。让出时间片会导致操作系统切换到另一个线程,这种上下文切换通常需要 10 微秒左右,而且至少需要两次切换。如果等待时间很短,比如只有几个微秒,忙等就比线程睡眠更高效。

可以看到,自旋锁和信号量的实现都非常简单,这也是两者的加解锁耗时分别排在第一和第二的原因。再次强调,加解锁耗时不能准确反应出锁的效率(比如时间片切换就无法发生),它只能从一定程度上衡量锁的实现复杂程度。


2.5 OSSpinLock自旋锁
#import <libkern/OSAtomic.h>
OSSpinLock *lock;

lock = OS_SPINLOCK_INIT;

- (void)lockSection {
    OSSpinLockLock(&lock);
    NSLog(@"before count: %ld", (long)count);
    count = count - 1;
    NSLog(@"after count: %ld", (long)count);
    OSSpinLockUnlock(&lock);
}

自旋锁的目的是为了确保临界区只有一个线程可以访问,它的使用可以用下面这段伪代码来描述:

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁
do {
    while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
    lock = true; // 挂上锁,这样别的线程就无法获得锁
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}

这段代码存在一个问题: 如果一开始有多个线程同时执行 while 循环,他们都不会在这里卡住,而是继续执行,这样就无法保证锁的可靠性了。解决思路也很简单,只要确保申请锁的过程是原子操作即可。

狭义上的原子操作表示一条不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完。在单处理器环境下,一条汇编指令显然是原子操作,因为中断也要通过指令来实现。

然而在多处理器的情况下,能够被多个处理器同时执行的操作任然算不上原子操作。因此,真正的原子操作必须由硬件提供支持,比如 x86 平台上如果在指令前面加上 “LOCK” 前缀,对应的机器码在执行时会把总线锁住,使得其他 CPU不能再执行相同操作,从而从硬件层面确保了操作的原子性。

※ 自旋锁的忙等

如果临界区的执行时间过长,使用自旋锁不是个好主意。之前我们介绍过时间片轮转算法,线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行 I/O 操作,或进入睡眠状态时,都会主动让出时间片。显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间。

OSSpinLock编译会报警告已经废弃了,大家也已经不再用它了,因为它在某一些场景下已经不安全了,可以参考不再安全的 OSSpinLock

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。

具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。


2.6 os_unfair_lock
#import <os/lock.h>
os_unfair_lock lock;
lock = OS_UNFAIR_LOCK_INIT;

- (void)lockSection {
    os_unfair_lock_lock(&lock);
    NSLog(@"before count: %ld", (long)count);
    count = count - 1;
    NSLog(@"after count: %ld", (long)count);
    os_unfair_lock_unlock(&lock);
}

os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用。os_unfair_lock是一种互斥锁,它不会向自旋锁那样忙等,而是等待线程会休眠。


2.7 pthread_mutex

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex 表示互斥锁。互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。

互斥锁的常见用法如下:

#include <pthread.h>

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) // 创建锁

pthread_mutex_lock(&mutex); // 申请锁
// 临界区
pthread_mutex_unlock(&mutex); // 释放锁

对于 pthread_mutex 来说,它的用法和之前没有太大的改变,比较重要的是锁的类型,可以有 PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE 等等。

  • PTHREAD_MUTEX_NORMAL 普通锁

  • PTHREAD_MUTEX_RECURSIVE 嵌套锁
    允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。

  • PTHREAD_MUTEX_ERRORCHECK 检错锁
    如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_NORMAL类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。

一般情况下,一个线程只能申请一次normal锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。

然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 pthread_mutex 支持递归锁,也就是允许一个线程递归的申请锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE 即可。


2.8 pthread_rwlock

读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态,要么是不加锁状态,而且一次只有一个线程对其加锁。

读写锁可以有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可用同时占有读模式的读写锁。

读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的,当它以写模式锁住时,它是以独占模式锁住的。

常用的接口有:

1、pthread_rwlock_init,初始化锁

2、pthread_rwlock_rdlock,阻断性的读锁定读写锁

3、pthread_rwlock_tryrdlock,非阻断性的读锁定读写锁

4、pthread_rwlock_wrlock,阻断性的写锁定读写锁

5、pthread_rwlock_trywrlock,非阻断性的写锁定读写锁

6、pthread_rwlock_unlock,解锁

7、pthread_rwlock_destroy,销毁锁释放

使用:

pthread_rwlock_t rwLock;

pthread_rwlock_init(&rwLock, NULL);

pthread_rwlock_wrlock(&rwLock);
// 写临界区
pthread_rwlock_unlock(&rwLock);

这里我感觉如果不用的时候最好能把锁销毁掉,否则可能会浪费内存吧。


2.9 NSCondition

The NSCondition class implements a condition variable whose semantics follow those used for POSIX-style conditions. A condition object acts as both a lock and a checkpoint in a given thread. The lock protects your code while it tests the condition and performs the task triggered by the condition. The checkpoint behavior requires that the condition be true before the thread proceeds with its task. While the condition is not true, the thread blocks. It remains blocked until another thread signals the condition object.

NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

使用:

NSConditon *condition =[ [NSCondition alloc]]init;

[condition lock];//一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问

[condition unlock];//与lock 同时使用

[condition wait];//让当前线程处于等待状态

[condition signal];//CPU发信号告诉线程不用在等待,可以继续执行
  • wait:阻塞住当前线程,线程会停在-wait方法中不会返回,直到被其他线程的-signal方法唤醒。
  • waitUntilDate:与-wait方法类似,不过这里多了一个时间参数表示最长阻塞到此时间为止
  • signal:调用此方法可以唤醒一个被-wait方法阻塞着的线程
  • broadcast:与-signal类似,不过这个方法可以唤醒所有被-wait方法阻塞着的线程

NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。

举个例子,生产者and消费者:

生产者:
-(void)produce{
    self.shouldProduce = YES;
    while (self.shouldProduce) {
        [self.condition lock];
        if (self.collector.count > 0 ) {
            [self.condition wait];
        }
        [self.collector addObject:@"iPhone"];
        NSLog(@"生产:iPhone");
        [self.condition signal];
        [self.condition unlock];
    }
}

消费者:
-(void)consumer{
    self.shouldConsumer = YES;
    while (self.shouldConsumer) {
        [self.condition lock];
        if (self.collector.count == 0 ) {
            [self.condition wait];
        }
        
        NSString *item = [self.collector objectAtIndex:0];
        NSLog(@"买入:%@",item);
        [self.collector removeObjectAtIndex:0];
        [self.condition signal];
        [self.condition unlock];
    }
}

wait做了什么?如何被唤醒?
当线程在wait一个condition时,condition对象会解锁它的lock,并阻塞线程。当condition被signaled,系统会唤醒线程。condition在wait()或者wait(until:)方法返回之前,会再次尝试获取到它的lock(如果获取lock不成功会继续保持阻塞状态)。因此,从线程的角度来看,就好像它总是held the lock

线程从-wait返回继续执行下面代码的前提是:此线程被其他signal方法唤醒 & 当前的condition不再被lock

一般在使用-wait时会使用一个谓词的修饰来做条件判断,这是因为:

  1. 根据苹果官方文档,-signal方法本身就不完全保证是准确的,会存在其他线程没有调用-signal方法,但是被wait的线程依然被唤醒的情况。

  2. 就算被wait的线程的唤醒时机没有问题,但是在被wait的线程被唤醒到执行后面代码期间,程序状态可能会发生变化,这也是一个风险项。所以在要执行wait后面代码时,都要重新判断当前程序状态是不是自己期望的。

如果有多个线程处在wait状态,那么它们被唤醒的顺序为先入先出,即先进入wait状态的线程先被唤醒。


一个错误:if or while来判断condition?

上面的例子中我们用了:

消费者:
if (self.collector.count == 0 ) {
  [self.condition wait];
}

并且在之后做了[self.collector removeObjectAtIndex:0];这个事儿。

但是呢注意如果想要从wait继续向下走,需要两个条件:一个是被signal唤醒,另一个是还需要获取lock。如果另外一个生产者在生产一个商品以后发了signal,然后又清空了产品,最后释放了锁。

那么,消费者在wait返回时,其实产品池还是空的,如果此时用if来判断self.collector.count == 0,那么其实走到下面的remove就会crash,但如果时用while,那么wait返回以后其实会再次判断count,如果产品池是空的,会再次wait不会直接跳出到后面的remove。

故而,在NSCondition的状况中,应该多用while来判断,而非if来判断需要wait的状况。


2.10 NSConditionLock

NSCondition与NSConditionLock非常的像,他们都需要3个元素:互斥锁,条件变量,条件探测变量。

不同点是NSCondition需要一个外部共享变量,来探测条件是否满足;而NSConditionLock不需要,条件锁自带一个探测条件,是否满足。

使用:

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}
//初始化一个NSConditionLock对象
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;  //锁的条件

//满足条件时加锁
- (void)lockWhenCondition:(NSInteger)condition;


- (BOOL)tryLock;
//如果接收对象的condition与给定的condition相等,则尝试获取锁,不阻塞线程
- (BOOL)tryLockWhenCondition:(NSInteger)condition;

//解锁后,重置锁的条件
- (void)unlockWithCondition:(NSInteger)condition;

- (BOOL)lockBeforeDate:(NSDate *)limit;

//在指定时间前尝试获取锁,若成功则返回YES 否则返回NO
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

NSConditionLock与NSCondition大体相同,但是NSConditionLock可以设置锁条件condition,而NSCondition确只是无脑的通知信号。

如果像下面这么用,让condition值永远是一样的话,其实就是NSLock:

lock = [[NSConditionLock alloc] initWithCondition:0];

[lock lockWhenCondition:0];
// 临界区
[lock unlockWithCondition:0];

如果维持其他的不变,将上锁改成[lock lockWhenCondition:1];那么其实lock之后的临界区就怎么也进不去了,因为condition初始化为0后从来也没被修改为1,所以始终wait无法被唤醒。

参考:
https://juejin.im/post/57f6e9f85bbb50005b126e5f#heading-1
https://www.jianshu.com/p/b1edc6b0937a
http://yulingtianxia.com/blog/2015/11/01/More-than-you-want-to-know-about-synchronized/
https://blog.csdn.net/TuGeLe/article/details/88399115
https://blog.csdn.net/chenyong05314/article/details/54598948
https://www.jianshu.com/p/5d20c15ae690
https://www.jianshu.com/p/25b00ce874c6

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

推荐阅读更多精彩内容

  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 656评论 0 0
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 789评论 0 2
  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,514评论 0 6
  • 目录:1.为什么要线程安全2.多线程安全隐患分析3.多线程安全隐患的解决方案4.锁的分类-13种锁4.1.1OSS...
    二斤寂寞阅读 1,180评论 0 3
  • 本周想说的主题是“牺牲”。起因是有人说我养狗牺牲了太多。在我的另一篇文章“一点不安”中也描述了这个问题。 思维发散...
    攻陷之神阅读 539评论 0 49