為什么需要使用鎖,當然熟悉多線程的你,自然不會感到陌生。
那你在代碼中是否很好的使用了鎖的機制呢?你又知道幾種實現鎖的方法呢?
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