白夜追凶,揭开iOS锁的秘密

百家 作者:iOS开发 2017-12-11 11:23:13

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!

前言

1. 时间片轮转调度算法这是目前操作系统中大量使用的线程管理方式,大致就是操作系统会给每个线程分配一段时间片(通常100ms左右)这些线程都被放在一个队列中,CPU只需要维护这个队列,当队首的线程时间片耗尽就会被强制放到队尾等待,然后提取下一个队首线程执行

2. 原子操作“原子”:一般指最小粒度,不可分割;原子操作也就是不可分割,不可中断的操作

3. 临界区 每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问

4. 忙等 试图进入临界区的线程,占着CPU而不释放的状态

5. 互斥锁如果一个线程无法获取互斥量,该线程会被直接挂起,不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活被挂起的线程。互斥锁会使得线程阻塞,阻塞的过程又分两个阶段,第一阶段是会先空转,可以理解成跑一个 while 循环,不断地去申请加锁,在空转一定时间之后,线程会进入 waiting 状态,此时线程就不占用CPU资源了,等锁可用的时候,这个线程会立即被唤醒

6. 自旋锁 如果一个线程需要获取自旋锁,该锁已经被其他线程占用,该线程不会被挂起,而是不断消耗CPU时间,一直试图获取自旋锁

NSLock

NSLock 是 Objective-C 以对象的形式暴露给开发者的一种锁, 它的组成结构是

  1. @protocol NSLocking

  2. - (void)lock;

  3. - (void)unlock;

  4. @end

  5. @interface NSLock : NSObject < NSLocking> {

  6. @private

  7.    void *_priv;

  8. }

  9. - (BOOL)tryLock;

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

  11. @property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

  12. @end

NSLock的内部实现就是通过宏来定义的lock方法

  1. #define    MLOCK

  2. - (void) lock

  3. {

  4.  int err = pthread_mutex_lock(&_mutex);

  5.  // 错误处理 ……

  6. }

NSLock只是在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,仅仅是内部 pthread_mutex互斥锁的类型不同。通过宏定义,可以简化方法的定义。 NSLockpthread_mutex略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响

举个?

  1. NSLock *lock = [[NSLock alloc] init];

  2.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  3.        //[lock lock];

  4.        [lock lockBeforeDate:[NSDate date]];

  5.            NSLog(@"需要线程同步的操作1 开始");

  6.            sleep(2);

  7.            NSLog(@"需要线程同步的操作1 结束");

  8.        [lock unlock];

  9.    });

  10.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  11.        sleep(1);

  12.        if ([lock tryLock]) {//尝试获取锁,如果获取不到返回NO,不会阻塞该线程

  13.            NSLog(@"锁可用的操作");

  14.            [lock unlock];

  15.        }else{

  16.            NSLog(@"锁不可用的操作");

  17.        }

  18.        NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];

  19.        if ([lock lockBeforeDate:date]) {//尝试在未来的3s内获取锁,并阻塞该线程,如果3s内获取不到恢复线程, 返回NO,不会阻塞该线程

  20.            NSLog(@"没有超时,获得锁");

  21.            [lock unlock];

  22.        }else{

  23.            NSLog(@"超时,没有获得锁");

  24.        }

  25.    });

  26. 20:45:08.864 SafeMultiThread[35911:575795] 需要线程同步的操作1 开始

  27. 20:45:09.869 SafeMultiThread[35911:575781] 锁不可用的操作

  28. 20:45:10.869 SafeMultiThread[35911:575795] 需要线程同步的操作1 结束

  29. 20:45:10.870 SafeMultiThread[35911:575781] 没有超时,获得锁

NSRecursiveLock 递归锁

NSRecursiveLock实际上定义的是一个递归锁,他和 NSLock 的区别在于, NSRecursiveLock 可以在一个线程中重复加锁(反正单线程内任务是按顺序执行的,不会出现资源竞争问题), NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。所以这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中, 其组成结构是

  1. @interface NSRecursiveLock : NSObject < NSLocking> {

  2. @private

  3.    void *_priv;

  4. }

  5. - (BOOL)tryLock;

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

  7. @property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

  8. @end

举个?

  1. NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  3.        static void (^RecursiveBlock)(int);

  4.        RecursiveBlock = ^(int value) {

  5.            [lock lock];

  6.            if (value > 0) {

  7.                NSLog(@"value:%d", value);

  8.                RecursiveBlock(value - 1);

  9.            }

  10.            [lock unlock];

  11.        };

  12.        RecursiveBlock(2);

  13.    });

  14. 14:43:12.327 ThreadLockControlDemo[1878:145003] value:2

  15. 14:43:12.327 ThreadLockControlDemo[1878:145003] value:1

前文我们已经介绍过,递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型。 NSRecursiveLockNSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE

NSConditionLock 条件锁

NSConditionLock的组成结构是

  1. @interface NSConditionLock : NSObject < NSLocking> {

  2. @private

  3.    void *_priv;

  4. }

  5. - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

  6. @property (readonly) NSInteger condition;

  7. - (void)lockWhenCondition:(NSInteger)condition;

  8. - (BOOL)tryLock;

  9. - (BOOL)tryLockWhenCondition:(NSInteger)condition;

  10. - (void)unlockWithCondition:(NSInteger)condition;

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

  12. - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

  13. @property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

  14. @end

NSConditionLockNSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 Condition 属性,以及每个操作都多了一个关于 Condition 属性的方法。例如 tryLocktryLockWhenCondition:NSConditionLock 可以称为条件锁,只有 Condition 参数与初始化时候的 Condition 相等, lock 才能正确进行加锁操作。而 unlockWithCondition: 并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值

举个?

  1. NSMutableArray *products = [NSMutableArray array];

  2.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  3.        while (1) {

  4.            [lock lockWhenCondition:0];

  5.            [products addObject:[[NSObject alloc] init]];

  6.            NSLog(@"produce a product,总量:%zi",products.count);

  7.            [lock unlockWithCondition:1];

  8.            sleep(1);

  9.        }

  10.    });

  11.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  12.        while (1) {

  13.            NSLog(@"wait for product");

  14.            [lock lockWhenCondition:1];

  15.            [products removeObjectAtIndex:0];

  16.            NSLog(@"custome a product");

  17.            [lock unlockWithCondition:0];

  18.        }

  19.    });

  20. 20:31:58.699 SafeMultiThread[31282:521698] wait for product

  21. 20:31:58.699 SafeMultiThread[31282:521708] produce a product,总量:1

  22. 20:31:58.700 SafeMultiThread[31282:521698] custome a product

  23. 20:31:58.700 SafeMultiThread[31282:521698] wait for product

  24. 20:31:59.705 SafeMultiThread[31282:521708] produce a product,总量:1

  25. 20:31:59.706 SafeMultiThread[31282:521698] wait for product

  26. 20:32:00.707 SafeMultiThread[31282:521708] produce a product,总量:1

  27. 20:32:00.708 SafeMultiThread[31282:521698] custome a product

NSCondition

NSCondition的底层是通过条件变量(Condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程。其组成结构是

  1. @interface NSCondition : NSObject < NSLocking> {

  2. @private

  3.    void *_priv;

  4. }

  5. - (void)wait;

  6. - (BOOL)waitUntilDate:(NSDate *)limit;

  7. - (void)signal;

  8. - (void)broadcast;

  9. @property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

  10. @end

需要注意的是pthread_cond_t要与互斥锁一起使用,为了保证数据的安全性

  1. void consumer () { // 消费者

  2.    pthread_mutex_lock(&mutex);

  3.    while (data == NULL) {

  4.        pthread_cond_wait(&condition_variable_signal, &mutex); // 等待数据

  5.    }

  6.    // temp = data;

  7.    pthread_mutex_unlock(&mutex);

  8. }

  9. void producer () {

  10.    pthread_mutex_lock(&mutex);

  11.    // 生产数据

  12.    pthread_cond_signal(&condition_variable_signal); // 发出信号给消费者,告诉他们有了新的数据

  13.    pthread_mutex_unlock(&mutex);

  14. }

总结一下使用条件变量的原因:

信号量可以一定程度上替代 Condition,但是互斥锁不行。在以上给出的生产者-消费者模式的代码中, pthread_cond_wait 方法的本质是锁的转移,消费者放弃锁,然后生产者获得锁,同理, pthread_cond_signal 则是一个锁从生产者到消费者转移的过程。

如果使用互斥锁,我们需要把代码改成这样:

  1. void consumer () { // 消费者

  2.    pthread_mutex_lock(&mutex);

  3.    while (data == NULL) {

  4.        pthread_mutex_unlock(&mutex);

  5.        pthread_mutex_lock(&another_lock)  // 相当于 wait 另一个互斥锁

  6.        pthread_mutex_lock(&mutex);

  7.    }

  8.    pthread_mutex_unlock(&mutex);

  9. }

这样做存在的问题在于,在等待 another_lock 之前, 生产者有可能先执行代码, 从而释放了 another_lock。也就是说,我们无法保证释放锁和等待另一个锁这两个操作是原子性的,也就无法保证“先等待、后释放 another_lock” 这个顺序。

用信号量则不存在这个问题,因为信号量的等待和唤醒并不需要满足先后顺序,信号量只表示有多少个资源可用,因此不存在上述问题。然而与 pthread_cond_wait 保证的原子性锁转移相比,使用信号量似乎存在一定风险(暂时没有查到非原子性操作有何不妥)。

不过,使用 Condition 有一个好处,我们可以调用 pthread_cond_broadcast 方法通知所有等待中的消费者,这是使用信号量无法实现的。

NSCondition 其实是封装了一个互斥锁和条件变量, 它把前者的 lock 方法和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者

  1. - (void) signal {

  2.  pthread_cond_signal(&_condition);

  3. }

  4. // 其实这个函数是通过宏来定义的,展开后就是这样

  5. - (void) lock {

  6.  int err = pthread_mutex_lock(&_mutex);

  7. }

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

[condition signal];CPU发信号告诉线程不用在等待,可以继续执行

需要注意的是: NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,经测试, NSCondition 并不会像上文的那些锁一样,先轮询,而是直接进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。

NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)

使用模型是:

1.锁定条件对象。

2.测试是否可以安全的履行接下来的任务。

3.如果布尔值是假的,调用条件对象的 waitwaitUntilDate:方法来阻塞线程。 在从这些方法返回,则转到步骤 2 重新测试你的布尔值。 (继续等待信号和重新测试,直到可以安全的履行接下来的任务。 waitUntilDate: 方法有个等待时间限制,指定的时间到了,则放回 NO,继续运行接下来的任务)

4.如果布尔值为真,执行接下来的任务。

5.当任务完成后,解锁条件对象

举个?

  1. NSCondition *condition = [[NSCondition alloc] init];

  2.    NSMutableArray *products = [NSMutableArray array];

  3.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  4.        while (1) {

  5.            [condition lock];

  6.            if ([products count] == 0) {

  7.                NSLog(@"wait for product");

  8.                [condition wait];

  9.            }

  10.            [products removeObjectAtIndex:0];

  11.            NSLog(@"custome a product");

  12.            [condition unlock];

  13.        }

  14.    });

  15.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  16.        while (1) {

  17.            [condition lock];

  18.            [products addObject:[[NSObject alloc] init]];

  19.            NSLog(@"produce a product,总量:%zi",products.count);

  20.            [condition signal];

  21.            [condition unlock];

  22.            sleep(1);

  23.        }

  24.    });

  25. 20:21:25.295 SafeMultiThread[31256:513991] wait for product

  26. 20:21:25.296 SafeMultiThread[31256:513994] produce a product,总量:1

  27. 20:21:25.296 SafeMultiThread[31256:513991] custome a product

  28. 20:21:25.297 SafeMultiThread[31256:513991] wait for product

  29. 20:21:26.302 SafeMultiThread[31256:513994] produce a product,总量:1

  30. 20:21:26.302 SafeMultiThread[31256:513991] custome a product

  31. 20:21:26.302 SafeMultiThread[31256:513991] wait for product

  32. 20:21:27.307 SafeMultiThread[31256:513994] produce a product,总量:1

  33. 20:21:27.308 SafeMultiThread[31256:513991] custome a product

@synchronized

@synchronized指令是在Objective-C代码中快速创建互斥锁的一种便捷方式。 @synchronized指令执行其他任何互斥锁都会执行的操作 - 它可以防止不同线程同时获取同一个锁。 但是,在这种情况下,您不必直接创建互斥锁或锁定对象。 相反,您只需使用任何Objective-C对象作为锁定标记。

传递给 @synchronized指令的对象是用于区分受保护块的唯一标识符。如果在两个不同的线程中执行上述方法,则在每个线程上为anObj参数传递一个不同的对象,每个线程都会锁定并继续处理而不被另一个线程阻塞。但是,如果在两种情况下传递相同的对象,则其中一个线程将首先获取锁,另一个会阻塞,直到第一个线程完成临界区。

作为预防措施, @synchronized块隐式地向受保护的代码添加一个异常处理程序。如果引发异常,该处理程序会自动释放互斥锁。这意味着为了使用 @synchronized指令,还必须在代码中启用Objective-C异常处理。如果您不想由隐式异常处理程序引起额外开销,则应考虑使用锁类。

@synchronized 其内部实现大概是这样的

  1. @try {

  2.    objc_sync_enter(obj);

  3.    // do work

  4. } @finally {

  5.    objc_sync_exit(obj);    

  6. }

当你调用 objc_sync_enter(obj) 时,它用 obj 内存地址的哈希值查找合适的 SyncData,然后将其上锁。当你调用 objc_sync_exit(obj) 时,它查找合适的 SyncData 并将其解锁。其中 SyncData的内部实现是:

  1. typedef struct SyncData {

  2.    id object;

  3.    recursive_mutex_t mutex;

  4.    struct SyncData* nextData;

  5.    int threadCount;

  6. } SyncData;

  7. typedef struct SyncList {

  8.    SyncData *data;

  9.    spinlock_t lock;

  10. } SyncList;

  11. // Use multiple parallel lists to decrease contention among unrelated objects.

  12. #define COUNT 16

  13. #define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))

  14. #define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock

  15. #define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data

  16. static SyncList sDataLists[COUNT];

object 就是我们给 @synchronized 传入的那个对象, object 为该锁的唯一标识,只有当标识相同时,才满足互斥。内部 object 被释放或被设为 nil,从我做的测试的结果来看,的确没有问题,但如果 object 一开始就是 nil,则失去了锁的功能。不过虽然 nil 不行,但 @synchronized([NSNull null]) 是完全可以的

mutex 它就是那个跟 object 关联在一起的锁

nextData 每个 SyncData 也包含一个指向另一个 SyncData 对象的指针, 所以你可以把每个 SyncData 结构体看做是链表中的一个元素

threadCount 这个 SyncData 对象中的锁会被一些线程使用或等待,threadCount 就是此时这些线程的数量。它很有用处,因为 SyncData 结构体会被缓存, threadCount==0 就暗示了这个 SyncData 实例可以被复用。

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

sDataLists 的声明 - 一个 SyncList 结构体数组,大小为16。通过定义的一个哈希算法将传入对象映射到数组上的一个下标。值得注意的是这个哈希算法设计的很巧妙,是将对象指针在内存的地址转化为无符号整型并右移五位,再跟 0xF 做按位与运算,这样结果不会超出数组大小。 LOCK_FOR_OBJ(obj)LIST_FOR_OBJ(obj) 这俩宏就更好理解了,先是哈希出对象的数组下标,然后取出数组对应元素的 lock 或 data。一切都是这么顺理成章哈。

举个?

  1. NSObject *obj = [[NSObject alloc] init];

  2.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  3.        @synchronized(obj) {

  4.            NSLog(@"需要线程同步的操作1 开始");

  5.            sleep(3);

  6.            NSLog(@"需要线程同步的操作1 结束");

  7.        }

  8.    });

  9.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  10.        sleep(1);

  11.        @synchronized(obj) {

  12.            NSLog(@"需要线程同步的操作2");

  13.        }

  14.    });

  15. 20:48:35.747 SafeMultiThread[35945:580107] 需要线程同步的操作1 开始

  16. 20:48:38.748 SafeMultiThread[35945:580107] 需要线程同步的操作1 结束

  17. 20:48:38.749 SafeMultiThread[35945:580118] 需要线程同步的操作2

dispatch_semaphore

dispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。他们的代码是:

  1. dispatch_semaphore_create(long value);

  2. dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

  3. dispatch_semaphore_signal(dispatch_semaphore_t dsema);

dispatch_semaphoreNSCondition 类似,都是一种基于信号的同步方式, dispatch_semaphore 能保存发送的信号。 dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。

dispatch_semaphore_t 的实现原理会调用最终会调用到 sem_postsem_wait 方法, 它的实现方法是:

  1. int sem_wait (sem_t *sem) {

  2.  int *futex = (int *) sem;

  3.  if (atomic_decrement_if_positive (futex) > 0)

  4.    return 0;

  5.  int err = lll_futex_wait (futex, 0);

  6.    return -1;

  7. )

  8. atomic_decrement_if_positive()的语义就是如果传入参数是正数就将其原子性的减一并立即返回。如果信号量为正,在Semaphores的语义中意味着没有竞争发生,如果没有竞争,就给信号量减一后直接返回了。

  9. 如果传入参数不是正数,即意味着有竞争,调用lll_futex_wait(futex,0)

首先会把信号量的值减一,并判断是否大于零。如果大于零,说明不用等待,所以立刻返回。具体的等待操作在 lll_futex_wait函数中实现, llllow levellock 的简称。这个函数通过汇编代码实现,调用到 SYS_futex这个系统调用,使线程进入睡眠状态,主动让出时间片,这个函数在互斥锁的实现中,也有可能被用到。主动让出时间片并不总是代表效率高。让出时间片会导致操作系统切换到另一个线程,这种上下文切换通常需要 10 微秒左右,而且至少需要两次切换。如果等待时间很短,比如只有几个微秒,忙等就比线程睡眠更高效。可以看到,自旋锁和信号量的实现都非常简单,这也是两者的加解锁耗时分别排在第一和第二的原因。再次强调,加解锁耗时不能准确反应出锁的效率(比如时间片切换就无法发生),它只能从一定程度上衡量锁的实现复杂程度

sem_post函数的作用是给信号量的值加上一个“1”,它是一个“原子操作"即同时对同一个信号量做加“1”操作的两个线程是不会冲突的;而同时对同一个文件进行读、加和写操作的两个程序就有可能会引起冲突。信号量的值永远会正确地加一个“2”--因为有两个线程试图改变它。

sem_wait函数也是一个原子操作,它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。也就是说,如果你对一个值为2的信号量调用 sem_wait(),线程将会继续执行,信号量的值将减到1。如果对一个值为0的信号量调用 sem_wait(),这个函数就会地等待直到有其它线程增加了这个值使它不再是0为止。如果有两个线程都在 sem_wait()中等待同一个信号量变成非零值,那么当它被第三个线程增加一个“1”时,等待线程中只有一个能够对信号量做减法并继续执行,另一个还将处于等待状态。

dispatch_semaphore_create(1) 方法可以创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始值为 1。注意,这里的传入的参数必须大于或等于 0,否则 dispatch_semaphore_create 会返回 NULL。而且只有 dispatch_semaphore_create 为 1 的时候才能当做锁来用。如果 dispatch_semaphore 的信号量初始值为 x ,则可以有 x 个线程同时访问被保护的临界区

dispatch_semaphore_wait (signal, overTime); 方法会判断 signal 的信号值是否大于 0。大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。

dispatch_semaphore_signal(signal); 发送信号,如果没有等待的线程接受信号,则使 signal 信号值加一(做到对信号的保存)

在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。

举个?

  1. dispatch_semaphore_t signal = dispatch_semaphore_create(1);

  2.    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);

  3.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  4.        dispatch_semaphore_wait(signal, overTime);

  5.            NSLog(@"需要线程同步的操作1 开始");

  6.            sleep(2);

  7.            NSLog(@"需要线程同步的操作1 结束");

  8.        dispatch_semaphore_signal(signal);

  9.    });

  10.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  11.        sleep(1);

  12.        dispatch_semaphore_wait(signal, overTime);

  13.            NSLog(@"需要线程同步的操作2");

  14.        dispatch_semaphore_signal(signal);

  15.    });

  16. 20:47:52.324 SafeMultiThread[35945:579032] 需要线程同步的操作1 开始

  17. 20:47:55.325 SafeMultiThread[35945:579032] 需要线程同步的操作1 结束

  18. 20:47:55.326 SafeMultiThread[35945:579033] 需要线程同步的操作2

  19. 如果把超时时间设置为< 2s的时候,执行的结果就是:

  20. 18:53:24.049 SafeMultiThread[30834:434334] 需要线程同步的操作1 开始

  21. 18:53:25.554 SafeMultiThread[30834:434332] 需要线程同步的操作2

  22. 18:53:26.054 SafeMultiThread[30834:434334] 需要线程同步的操作1 结束

pthread_mutex (又称POSIX互斥锁)

pthread_mutex 是 C 语言下多线程加互斥锁的方式,你结构如下:

  1. int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);

  2. int pthread_mutex_lock(pthread_mutex_t *);

  3. int pthread_mutex_trylock(pthread_mutex_t *);

  4. int pthread_mutex_unlock(pthread_mutex_t *);

  5. int pthread_mutex_destroy(pthread_mutex_t *);

  6. int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,

  7.  int * __restrict);

  8. int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,

  9.  int * __restrict);

互斥锁有2种初始化方式:

第一种是静态方式加锁: pthread_mutex_tmutex=PTHREAD_MUTEX_INITIALIZER; 全局变量或者static变量

第二种是动态方式加锁: pthread_mutex_init(pthread_mutex_t,constpthread_mutexattr_t)pthread_mutexattr_t*为属性

pthread_mutex_init 这是初始化一个锁, pthread_mutex_t 为互斥锁的类型,传 NULL 为默认类型,一共有 4 类型

  1. PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。

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

  3. PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。

  4. PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。

pthread_mutexattr_t 可以用来设置锁的类型,比如递归锁

  1. pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)

pthread_mutex_trylock 方法, pthread_mutex_trylocktryLock 的区别在于, tryLock 返回的是 YES 和 NO, pthread_mutex_trylock 加锁成功返回的是 0,失败返回的是错误提示码。

  1. pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr);

  2. 初始化锁变量mutex。attr为锁属性,NULL值为默认属性。

  3. pthread_mutex_lock(pthread_mutex_t mutex);加锁

  4. pthread_mutex_tylock(*pthread_mutex_t *mutex);加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。

  5. pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁

  6. pthread_mutex_destroy(pthread_mutex_t* mutex);使用完后释放,常用于递归锁的时候

  7. pthread_mutexattr_setpshared 设置互斥锁范围语法

  8. pthread_mutexattr_getpshared 获取互斥锁范围语法

举个?

  1.  __block pthread_mutex_t theLock;

  2.    //pthread_mutex_init(&theLock, NULL);

  3.    pthread_mutexattr_t attr;

  4.    pthread_mutexattr_init(&attr);

  5.    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

  6.    pthread_mutex_init(&lock, &attr);

  7.    pthread_mutexattr_destroy(&attr);

  8.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  9.        static void (^RecursiveMethod)(int);

  10.        RecursiveMethod = ^(int value) {

  11.            pthread_mutex_lock(&theLock);

  12.            if (value > 0) {

  13.                NSLog(@"value = %d", value);

  14.                sleep(1);

  15.                RecursiveMethod(value - 1);

  16.            }

  17.            pthread_mutex_unlock(&theLock);

  18.        };

  19.        RecursiveMethod(5);

  20.    });

OSSpinLock (已被苹果弃用)

OSSpinLock 常用的结构形式

  1. typedef int32_t OSSpinLock;

  2. bool    OSSpinLockTry( volatile OSSpinLock *__lock );

  3. void    OSSpinLockLock( volatile OSSpinLock *__lock );

  4. void    OSSpinLockUnlock( volatile OSSpinLock *__lock );

OSSpinLock 是一种自旋锁 和 NSLock 不同的是 NSLock 请求加锁失败的话,会先轮询,但一秒过后便会使线程进入 waiting 状态,等待唤醒。而 OSSpinLock 会一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。

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

自旋锁适合短时间的操作,加锁性能最快,但不能使用不同优先级。

举个?

  1. __block OSSpinLock theLock = OS_SPINLOCK_INIT;

  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  3.    OSSpinLockLock(&theLock);

  4.    NSLog(@"需要线程同步的操作1 开始");

  5.    sleep(3);

  6.    NSLog(@"需要线程同步的操作1 结束");

  7.    OSSpinLockUnlock(&theLock);

  8. });

  9. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  10.    OSSpinLockLock(&theLock);

  11.    sleep(1);

  12.    NSLog(@"需要线程同步的操作2");

  13.    OSSpinLockUnlock(&theLock);

  14. });

总结一下

根据锁的实现机制还有加解锁的速度,我们可以总结一下:如果应用在递归场景的时候,我们有2种选择,一个是 NSRecursiveLock,另一个是 pthread_mutex(recursive)。追求高性能的话,优先选择 pthread_mutex,苹果已对 pthread_mutex进行过多次优化。


现在回到普通场景:不需要考虑性能的前提下,建议使用 @synchronized进行加锁,这种锁不需要使用者考虑加锁和解锁的问题,已经在内部实现过了,但是他是加锁,解锁速率最慢的。如果是要封装第三方库的话,建议使用 dispatch_semaphore或者 pthread_mutex来进行加锁和解锁的操作,其中YYkit框架在大篇幅的使用 dispatch_semaphore_t进行加锁和解锁。


在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。多资源分配,线程步调通知也不适合使用互斥锁进行分配。

参考文章

  • iOS中保证线程安全的几种方式与性能对比 [http://www.jianshu.com/p/938d68ed832c]

  • 不再安全的 OSSpinLock  [https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/]

  • 关于 @synchronized,这儿比你想知道的还要多 [http://yulingtianxia.com/blog/2015/11/01/More-than-you-want-to-know-about-synchronized/]

  • 深入理解iOS开发中的锁 [https://bestswifter.com/ios-lock/#]



  • 来自:杨恒占

  • 链接:https://mp.weixin.qq.com/s/e2VA7-E01OopF-ObPE3YOQ

  • iOS开发整理发布,转载请联系作者授权

【点击成为Android大神】

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接