iOS开发中多线程间关于锁的使用

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发中多线程间关于锁的使用相关的知识,希望对你有一定的参考价值。

为什么需要使用锁,当然熟悉多线程的你,自然不会感到陌生。

那你在代码中是否很好的使用了锁的机制呢?你又知道几种实现锁的方法呢?

 main.m

技术分享
 1 int main(int argc, const char * argv[]) {
 2     @autoreleasepool {
 3         //普通用法;会看到线程1锁住之后,线程2会一直等待,直到线程1执行完,线程2才执行
 4         NSLog(@"使用NSLock(普通锁;已实现NSLocking协议)实现锁");
 5         [LockByNSLock executeLock];
 6         
 7         NSLog(@"使用Synchronized指令实现锁");
 8         [LockBySynchronized executeLock];
 9         
10         NSLog(@"使用C语言的pthread_mutex_t实现锁");
11         [LockByPthreadMutexT executeLock];
12         
13         NSLog(@"使用GCD的dispatch_semaphore_t(信号量)实现锁");
14         [LockByDispatchSemaphoreT executeLock];
15         
16         
17         //高级用法
18         NSLog(@"使用NSRecursiveLock(递归锁;已实现NSLocking协议)实现锁;可以在递归场景中使用。如果使用NSLock,会出现死锁");
19         [LockByNSRecursiveLock executeLock];
20         
21         NSLog(@"使用NSConditionLock(条件锁;已实现NSLocking协议)实现锁;可以在需要符合条件才进行锁操作的场景中使用");
22         [LockByNSConditionLock executeLock];
23         
24         NSLog(@"使用NSDistributedLock(分布式锁;区别其他锁类型,它没有实现NSLocking协议)实现锁;它基于文件系统,会自动创建用于标识的临时文件或文件夹,执行完后自动清除临时文件或文件夹;可以在多个进程或多个程序之间需要构建互斥的场景中使用");
25         [LockByNSDistributedLock executeLock]; //这里看不出效果,具体测试应该是线程1和线程2同时进行,即是在多个进程或多个程序之间需要构建互斥的场景下
26     }
27     return 0;
28 }
技术分享

今天一起来探讨一下 ios 中实现锁的几种不同方式,在这之前我们先构建一个测试用的类,假想它是我们的一个共享资源,firstMethod 与 secondMethod 是互斥的,代码如下:

技术分享
 1 #import "TestObj.h"
 2 
 3 @implementation TestObj
 4 
 5 - (void)firstMethod {
 6     NSLog(@"Execute %@", NSStringFromSelector(_cmd));
 7 }
 8 
 9 - (void)secondMethod {
10     NSLog(@"Execute %@", NSStringFromSelector(_cmd));
11 }
12 
13 @end
技术分享

1.使用 NSLock 实现的锁

技术分享
 1 #import "LockByNSLock.h"
 2 #import "TestObj.h"
 3 
 4 @implementation LockByNSLock
 5 
 6 + (void)executeLock {
 7     //主线程中
 8     TestObj *obj = [[TestObj alloc] init];
 9     NSLock *lock = [[NSLock alloc] init];
10     
11     //线程1
12     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
13         [lock lock];
14         [obj firstMethod];
15         sleep(2); //线程1执行挂起2秒
16         [lock unlock];
17     });
18     
19     //线程2
20     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
21         sleep(1); //线程2执行挂起1秒,以保证一定是线程1先执行
22         [lock lock];
23         [obj secondMethod];
24         [lock unlock];
25     });
26 }
27 
28 @end
技术分享

看到打印的结果了吗,你会看到线程1锁住之后,线程2会一直等待走到线程1将锁置为 unlock 后,才会执行 secondMethod 方法。

NSLock 是 Cocoa 提供给我们最基本的锁对象,这也是我们经常所使用的,除 lock 和 unlock 方法外,NSLock 还提供了 tryLock 和 lockBeforeDate: 两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),并不会阻塞线程,直接返回 NO。lockBeforeDate: 方法会在所指定 Date 之前尝试加锁,如果在指定时间之前都不能加锁,则返回 NO。

 

2.使用 synchronized 关键字构建的锁

当然在 Objective-C 中你还可以用 @synchronized 指令快速的实现锁:

技术分享
 1 #import "LockBySynchronized.h"
 2 #import "TestObj.h"
 3 
 4 @implementation LockBySynchronized
 5 
 6 + (void)executeLock {
 7     //主线程中
 8     TestObj *obj = [[TestObj alloc] init];
 9     
10     //线程1
11     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
12         @synchronized(obj) {
13             [obj firstMethod];
14             sleep(2); //线程1执行挂起2秒
15         }
16     });
17     
18     //线程2
19     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
20         @synchronized(obj) {
21             sleep(1); //线程2执行挂起1秒,以保证一定是线程1先执行
22             [obj secondMethod];
23         }
24     });
25 }
26 
27 @end
技术分享

@synchronized 指令使用的 obj 为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj) 改为@synchronized(other) 时,线程2就不会被阻塞,@synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

 

3.使用C语言的 pthread_mutex_t 实现的锁

技术分享
 1 #import "LockByPthreadMutexT.h"
 2 #import "TestObj.h"
 3 #include <pthread.h>
 4 
 5 @implementation LockByPthreadMutexT
 6 
 7 + (void)executeLock {
 8     //主线程中
 9     TestObj *obj = [[TestObj alloc] init];
10     __block pthread_mutex_t mutex;
11     pthread_mutex_init(&mutex, NULL);
12     
13     //线程1
14     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
15         pthread_mutex_lock(&mutex);
16         [obj firstMethod];
17         sleep(2); //线程1执行挂起2秒
18         pthread_mutex_unlock(&mutex);
19     });
20     
21     //线程2
22     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
23         sleep(1); //线程2执行挂起1秒,以保证一定是线程1先执行
24         pthread_mutex_lock(&mutex);
25         [obj secondMethod];
26         pthread_mutex_unlock(&mutex);
27     });
28 }
29 
30 @end
技术分享

pthread_mutex_t 定义在pthread.h,所以记得 #include

 

4.使用 GCD 来实现的”锁”
以上代码构建多线程我们就已经用到了 GCD 的 dispatch_async 方法,其实在 GCD 中也已经提供了一种信号机制,使用它我们也可以来构建一把”锁”

从本质意义上讲,信号量与互斥锁是有区别:

(1)作用域 

信号量:进程间或线程间(linux 仅线程间) 

互斥锁:线程间

 

(2)上锁时

信号量:只要信号量的 value 大于0,其他线程就可以 sem_wait 成功,成功后信号量的 value 减一。若 value 值不大于0,则 sem_wait 阻塞,直到 sem_post 释放后 value 值加一。一句话,信号量的 value>=0。 

互斥锁:只要被锁住,其他任何线程都不可以访问被保护的资源。如果没有锁,获得资源成功,否则进行阻塞等待资源可用。一句话,线程互斥锁的 vlaue 可以为负数。 

技术分享
 1 #import "LockByDispatchSemaphoreT.h"
 2 #import "TestObj.h"
 3 
 4 @implementation LockByDispatchSemaphoreT
 5 
 6 + (void)executeLock {
 7     //主线程中
 8     TestObj *obj = [[TestObj alloc] init];
 9     dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
10     
11     //线程1
12     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
13         dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
14         [obj firstMethod];
15         sleep(2); //线程1执行挂起2秒
16         dispatch_semaphore_signal(semaphore);
17     });
18     
19     //线程2
20     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
21         sleep(1); //线程2执行挂起1秒,以保证一定是线程1先执行
22         dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
23         [obj secondMethod];
24         dispatch_semaphore_signal(semaphore);
25     });
26 }
27 
28 @end
技术分享

至于代码产生的效果当然和前面的是一模一样的,当然锁大多数情况下也是配合多线程一起使用的。

 

 

锁的高级用法

1.NSRecursiveLock递归锁

平时我们在代码中使用锁的时候,最容易犯的一个错误就是造成死锁,而容易造成死锁的一种情形就是在递归或循环中,如下代码:

技术分享
 1 #import "LockByNSRecursiveLock.h"
 2 #import "TestObj.h"
 3 
 4 @implementation LockByNSRecursiveLock
 5 
 6 + (void)executeLock {
 7     //主线程中
 8     TestObj *obj = [[TestObj alloc] init];
 9     //NSLock *lock = [[NSLock alloc] init]; //NSLock在递归场景会出现死锁的情况,这里就得用NSRecursiveLock(递归锁)
10     NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
11     
12     //线程1
13     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
14         static void(^recursiveMethod)(NSUInteger);
15         recursiveMethod = ^(NSUInteger val) {
16             [lock lock];
17             if (val > 0) {
18                 NSLog(@"递归中,val的值为:%lu", (unsigned long)val);
19                 [obj firstMethod];
20                 sleep(2); //线程1执行挂起2秒
21                 recursiveMethod(val-1);
22             }
23             [lock unlock];
24         };
25         
26         recursiveMethod(5);
27     });
28     
29     //线程2
30     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
31         sleep(1); //线程2执行挂起1秒,以保证一定是线程1先执行
32         [lock lock];
33         [obj secondMethod];
34         [lock unlock];
35     });
36 }
37 
38 @end
技术分享

以上的代码中,就是一种典型的死锁情况,因为在线程1中的递归 block 中,锁会被多次的 lock,所以自己也被阻塞了,由于以上的代码非常的简短,所以很容易能识别死锁,但在较为复杂的代码中,就不那么容易发现了,那么如何在递归或循环中正确的使用锁呢?此处的 lock 变量如果换用 NSRecursiveLock 对象,问题便得到解决了,NSRecursiveLock 类定义的锁可以在同一线程多次 lock,而不会造成死锁。递归锁会跟踪它被多少次 lock。每次成功的 lock 都必须平衡调用 unlock 操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。

 

2.NSConditionLock 条件锁

当我们在使用多线程的时候,有时一把只会 lock 和 unlock 的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数情况是只有满足一定条件的情况下才能打开这把锁:

技术分享
 1 #import "LockByNSConditionLock.h"
 2 #import "TestObj.h"
 3 
 4 @implementation LockByNSConditionLock
 5 
 6 + (void)executeLock {
 7     //主线程中
 8     TestObj *obj = [[TestObj alloc] init];
 9     NSConditionLock *lock = [[NSConditionLock alloc] init];
10     
11     //线程1
12     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
13         for (NSUInteger i=0; i<3; i++) {
14             [lock lock];
15             NSLog(@"循环中,i的值为:%lu", (unsigned long)i);
16             [obj firstMethod];
17             sleep(2); //线程1执行挂起2秒
18             [lock unlockWithCondition:i];
19         }
20     });
21     
22     //线程2
23     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
24         sleep(1); //线程2执行挂起1秒,以保证一定是线程1先执行
25         //[lock lockWhenCondition:2]; //需要标识为2的钥匙才能进行加锁操作,这里等待线程1对lock的循环操作达到它需要的加锁条件;如果最终不符合条件(例如:线程1的条件判断是i<4时),将阻塞线程内容向下执行;这里就得用tryLockWhenCondition:方法控制
26         //[obj secondMethod];
27         //[lock unlock];
28         
29         BOOL isLocked = [lock tryLockWhenCondition:2];
30         [obj secondMethod];
31         if (isLocked) { //加锁解锁必须成对操作,否则会报错
32             [lock unlock];
33         }
34     });
35 }
36 
37 @end
技术分享

在线程1中的加锁使用了 lock,所以是不需要条件的,所以顺利的就锁住了,但在 unlock 的使用了一个整型的条件,它可以开启其他线程中正在等待这把钥匙的临界地,而线程2则需要一把被标识为2的钥匙,所以当线程1循环到最后一次的时候,才最终打开了线程2中的阻塞。但即便如此,NSConditionLock 也跟其他的锁一样,是需要 lock 与 unlock 对应的,只是 lock,lockWhenCondition: 与 unlock,unlockWithCondition: 是可以随意组合的,当然这是与你的需求相关的。

 

3.NSDistributedLock 分布式锁

以上所有的锁都是在解决多线程之间的冲突,但如果遇上多个进程或多个程序之间需要构建互斥的情景该怎么办呢?这个时候我们就需要使用到 NSDistributedLock 了,从它的类名就知道这是一个分布式的 Lock,NSDistributedLock 的实现是通过文件系统的,所以使用它才可以有效的实现不同进程之间的互斥,但 NSDistributedLock 并非继承于 NSLock,它没有 lock 方法,它只实现了 tryLock, unlock, breakLock,所以如果需要 lock 的话,你就必须自己实现一个 tryLock 的轮询,下面通过代码简单的演示一下吧:

技术分享
 1 #import "LockByNSDistributedLock.h"
 2 #import "TestObj.h"
 3 
 4 @implementation LockByNSDistributedLock
 5 
 6 + (void)executeLock {
 7     //主线程中
 8     TestObj *obj = [[TestObj alloc] init];
 9     NSDistributedLock *lock = [[NSDistributedLock alloc] initWithPath:@"/Users/Kenmu/Desktop/Temp/LockByNSDistributedLock"];
10     
11     //线程1
12     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
13         [lock breakLock];
14         [lock tryLock];
15         [obj firstMethod];
16         sleep(5); //线程1执行挂起5秒
17         [lock unlock];
18     });
19     
20     //线程2
21     dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
22         while (![lock tryLock]) {
23             NSLog(@"Waiting...");
24             sleep(1);
25         }
26         [obj secondMethod];
27         [lock unlock];
28     });
29 }
30 
31 @end
技术分享

实际场景应该如下:

程序A:

技术分享
1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
2     lock = [[NSDistributedLock alloc] initWithPath:@"/Users/Kenmu/Desktop/earning__"];
3     [lock breakLock];
4     [lock tryLock];
5     sleep(10);
6     [lock unlock];
7     NSLog(@"appA: OK");
8 });
技术分享

程序B:

技术分享
1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
2     lock = [[NSDistributedLock alloc] initWithPath:@"/Users/Kenmu/Desktop/earning__"];
3     while (![lock tryLock]) {
4         NSLog(@"appB: waiting");
5         sleep(1);
6     }
7     [lock unlock];
8     NSLog(@"appB: OK");
9 });
技术分享

先运行程序 A,然后立即运行程序 B,根据打印你可以清楚的发现,当程序 A 刚运行的时候,程序 B 一直处于等待中,当大概10秒过后,程序 B 便打印出了 appB:OK 的输出,以上便实现了两上不同程序之间的互斥。/Users/Kenmu/Desktop/earning __是一个文件或文件夹的地址,如果该文件或文件夹不存在,那么在 tryLock 返回 YES 时,会自动创建该文件/文件夹。在结束的时候该文件/文件夹会被清除,所以在选择的该路径的时候,应该选择一个不存在的路径,以防止误删了文件。

 

结果:

技术分享
 1 2015-05-15 00:29:43.046 OCLock[1675:61800] 使用NSLock(普通锁;已实现NSLocking协议)实现锁
 2 2015-05-15 00:29:43.047 OCLock[1675:61800] Execute firstMethod
 3 2015-05-15 00:29:46.055 OCLock[1675:61800] Execute secondMethod
 4 2015-05-15 00:29:46.055 OCLock[1675:61800] 使用Synchronized指令实现锁
 5 2015-05-15 00:29:46.055 OCLock[1675:61800] Execute firstMethod
 6 2015-05-15 00:29:49.060 OCLock[1675:61800] Execute secondMethod
 7 2015-05-15 00:29:49.061 OCLock[1675:61800] 使用C语言的pthread_mutex_t实现锁
 8 2015-05-15 00:29:49.061 OCLock[1675:61800] Execute firstMethod
 9 2015-05-15 00:29:52.067 OCLock[1675:61800] Execute secondMethod
10 2015-05-15 00:29:52.067 OCLock[1675:61800] 使用GCD的dispatch_semaphore_t(信号量)实现锁
11 2015-05-15 00:29:52.068 OCLock[1675:61800] Execute firstMethod
12 2015-05-15 00:29:55.078 OCLock[1675:61800] Execute secondMethod
13 2015-05-15 00:29:55.079 OCLock[1675:61800] 使用NSRecursiveLock(递归锁;已实现NSLocking协议)实现锁;可以在递归场景中使用。如果使用NSLock,会出现死锁
14 2015-05-15 00:29:55.079 OCLock[1675:61800] 递归中,val的值为:5
15 2015-05-15 00:29:55.079 OCLock[1675:61800] Execute firstMethod
16 2015-05-15 00:29:57.080 OCLock[1675:61800] 递归中,val的值为:4
17 2015-05-15 00:29:57.080 OCLock[1675:61800] Execute firstMethod
18 2015-05-15 00:29:59.083 OCLock[1675:61800] 递归中,val的值为:3
19 2015-05-15 00:29:59.084 OCLock[1675:61800] Execute firstMethod
20 2015-05-15 00:30:01.089 OCLock[1675:61800] 递归中,val的值为:2
21 2015-05-15 00:30:01.089 OCLock[1675:61800] Execute firstMethod
22 2015-05-15 00:30:03.095 OCLock[1675:61800] 递归中,val的值为:1
23 2015-05-15 00:30:03.095 OCLock[1675:61800] Execute firstMethod
24 2015-05-15 00:30:06.106 OCLock[1675:61800] Execute secondMethod
25 2015-05-15 00:30:06.106 OCLock[1675:61800] 使用NSConditionLock(条件锁;已实现NSLocking协议)实现锁;可以在需要符合条件才进行锁操作的场景中使用
26 2015-05-15 00:30:06.107 OCLock[1675:61800] 循环中,i的值为:0
27 2015-05-15 00:30:06.107 OCLock[1675:61800] Execute firstMethod
28 2015-05-15 00:30:08.112 OCLock[1675:61800] 循环中,i的值为:1
29 2015-05-15 00:30:08.113 OCLock[1675:61800] Execute firstMethod
30 2015-05-15 00:30:10.115 OCLock[1675:61800] 循环中,i的值为:2
31 2015-05-15 00:30:10.115 OCLock[1675:61800] Execute firstMethod
32 2015-05-15 00:30:13.121 OCLock[1675:61800] Execute secondMethod
33 2015-05-15 00:30:13.121 OCLock[1675:61800] 使用NSDistributedLock(分布式锁;区别其他锁类型,它没有实现NSLocking协议)实现锁;它基于文件系统,会自动创建用于标识的临时文件或文件夹,执行完后自动清除临时文件或文件夹;可以在多个进程或多个程序之间需要构建互斥的场景中使用
34 2015-05-15 00:30:13.127 OCLock[1675:61800] Execute firstMethod
35 2015-05-15 00:30:18.129 OCLock[1675:61800] Execute secondMethod

以上是关于iOS开发中多线程间关于锁的使用的主要内容,如果未能解决你的问题,请参考以下文章

IOS开发中多线程的使用

Go中多协程协作之sync.Cond

iOS中多线程的使用

谈谈iOS多线程的锁

谈谈iOS多线程的锁

生产者消费者中多线程安全问题(即线程间通信的安全问题)