iOS開發進階:執行緒同步技術-鎖
在iOS多執行緒中,經常會出現資源競爭和死鎖的問題。本節將學習iOS中不同的鎖。
執行緒同步方案
常見的兩個問題:多執行緒買票和存取錢問題。
示例:存取錢問題
// 示例:存取錢問題 - (void)moneyTest { self.moneyCount = 100; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self saveMoney]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self takeMoney]; } }); } - (void)saveMoney { int oldCount = self.moneyCount; sleep(0.2); oldCount += 50; self.moneyCount = oldCount; NSLog(@"存50,還剩%d錢", self.moneyCount); } - (void)takeMoney { int oldCount = self.moneyCount; sleep(0.2); oldCount -= 20; self.moneyCount = oldCount; NSLog(@"取20,還剩%d錢", self.moneyCount); }
示例:賣票問題
// 示例:買票 - (void)sellTest { self.count = 15; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self printTest2]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self printTest2]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [self printTest2]; } }); } - (void)printTest2 { NSInteger oldCount = self.count; sleep(0.2); oldCount --; self.count = oldCount; NSLog(@"還剩%ld張票 - %@", (long)oldCount, [NSThread currentThread]); }
解決上面這種資源共享問題,就需要使用執行緒同步技術。執行緒同步技術的核心是:鎖。下面學習iOS中不同鎖的使用,比較不同鎖之間的優缺點。
示例程式碼: ofollow,noindex">演示購票和存取錢問題:Demo
iOS當中有哪些鎖?
@synchronized 常用於單例 atomic 原子性 OSSpinLock 自旋鎖 NSRecursiveLock 遞迴鎖 NSLock dispatch_semaphore_t 訊號量 NSCondition 條件 NSConditionLock 條件鎖
簡介:
-
@synchronized
使用場景:一般在建立單例物件時使用,保證物件在多執行緒中是唯一的。 -
atomic
屬性關鍵字原子性,保證賦值操作是執行緒安全的,讀取操作不能保證執行緒安全。 -
OSSpinLock
自旋鎖。特點:迴圈等待訪問,不釋放當前資源。常用於輕量級資料訪問,簡單的int值+1/-1操作。
*NSLock
某個執行緒A呼叫lock方法。這樣,NSLock將被上鎖。可以執行“關鍵部分”,完成後,呼叫unlock方法。如果,線上程A 呼叫unlock方法之前,另一個執行緒B呼叫了同一鎖物件的lock方法。那麼,執行緒B只有等待。直到執行緒A呼叫了unlock。
[lock lock]; //加鎖 // 關鍵部分 [lock unlock]; // 解鎖
-
NSRecursiveLock
遞迴鎖,特點:遞迴鎖在被同一執行緒重複獲取時不會產生死鎖。 -
dispatch_semaphore_t
訊號量// 建立訊號量結構體物件,含有一個int成員 dispatch_semaphore_create(1); // 先對value減一,如果小於零表示沒有資源可以訪問。通過主動行為進行阻塞。 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // value加1,小於等零表示有佇列在排隊,通過被動行為進行喚醒 dispatch_semaphore_signal(semaphore);
OSSpinLock
自旋鎖,等待鎖的執行緒會處於忙等狀態,一直佔用著CPU資源。
常用API:
匯入標頭檔案 #import <libkern/OSAtomic.h> // 初始化 OSSpinLock lock = OS_SPINLOCK_INIT; // 嘗試加鎖 OSSpinLockTry(&lock); // 加鎖 OSSpinLockLock(&lock); // 解鎖 OSSpinLockUnlock(&lock);
使用 OSSpinLock
解決賣票問題
// 自旋鎖:#import <libkern/OSAtomic.h> // 定義一個全域性的自旋鎖物件 lock 。 - (void)printTest2 { // 加鎖 OSSpinLockLock(&_lock); NSInteger oldCount = self.count; sleep(0.2); oldCount --; self.count = oldCount; NSLog(@"還剩%ld張票 - %@", (long)oldCount, [NSThread currentThread]); // 解鎖 OSSpinLockUnlock(&_lock); }
使用 OSSpinLock
解決存取錢問題
- (void)saveMoney { OSSpinLockLock(&_moneyLock); int oldCount = self.moneyCount; sleep(0.2); oldCount += 50; self.moneyCount = oldCount; NSLog(@"存50,還剩%d錢", self.moneyCount); OSSpinLockUnlock(&_moneyLock); } - (void)takeMoney { OSSpinLockLock(&_moneyLock); int oldCount = self.moneyCount; sleep(0.2); oldCount -= 20; self.moneyCount = oldCount; NSLog(@"取20,還剩%d錢", self.moneyCount); OSSpinLockUnlock(&_moneyLock); }
注意:賣票和取錢不要共用一把鎖。這裡建立了兩把鎖 sellLock
和 moneyLock
。
自旋鎖現在不再安全,因為可能出現優先順序反轉問題。如果等待鎖的執行緒優先順序較高,他會一直佔用CPU資源,優先順序低的執行緒就無法獲取CPU資源完成任務並釋放鎖。可以檢視這篇文章 不再安全的OSSpinLock 。
本節示例程式碼: 執行緒同步解決方案Demo
os_unfair_lock
自旋鎖已經不再安全,存在優先順序反轉問題。蘋果在iOS10開始使用 os_unfair_lock
取代了 OSSpinLock
。從底層呼叫來看,自旋鎖和 os_unfair_lock
的區別,前者等待執行緒處於忙等,而後者等待執行緒處於休眠狀態。
常用API:
匯入標頭檔案 #import <os/lock.h> // 初始化 os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; // 嘗試加鎖 os_unfair_lock_trylock(&_lock); // 加鎖 os_unfair_lock_lock(&_lock); // 解鎖 os_unfair_lock_unlock(&_lock);
pthread_mutex
互斥鎖,等待鎖的執行緒處於休眠狀態。
常用API:
// 標頭檔案 #import <pthread.h> - (void)__initLock:(pthread_mutex_t *)lock { // 初始化屬性 pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); // 設定為普通鎖,PTHREAD_MUTEX_RECURSIVE表示遞迴鎖 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); // 初始化鎖 pthread_mutex_init(lock, &attr); // 銷燬屬性 pthread_mutexattr_destroy(&attr); } // 加鎖 pthread_mutex_lock(&lock); // 解鎖 pthread_mutex_unlock(&lock); // 初始化條件 pthread_cond_init(&cond, NULL) // 等待條件(進入休眠,放開鎖;被喚醒後,會再次加鎖) pthread_cond_wait(&cond, &lock); // 啟用一個等待該條件的執行緒 pthread_cond_signal(&cond); // 啟用所有等待該條件的執行緒 pthread_cond_broadcast(&cond); // 銷燬資源 pthread_mutex_destory(&lock); pthread_cond_destory(&cond);
其中 PTHREAD_MUTEX_DEFAULT
設定的是鎖的型別,還有另一種型別 PTHREAD_MUTEX_RECURSIVE
表示遞迴鎖。遞迴鎖允許同一個執行緒對一把鎖進行重複加鎖。
NSLock&NSRecursiveLock&NSCondition
NSLock
是對 mutex
普通鎖的封裝。
@protocol NSLocking - (void)lock; - (void)unlock; @end @interface NSLock : NSObject <NSLocking> { - (BOOL)tryLock; // 嘗試加鎖 - (BOOL)lockBeforeDate:(NSDate *)limit; //在時間之前獲取鎖並返回,YES表示成功。 } @end
NSRecursiveLock
是對 mutex
遞迴鎖的封裝,API同 NSLock
相似。
NSCondition
是對 mutex
條件的封裝。
@protocol NSLocking - (void)lock; - (void)unlock; @end @interface NSCondition : NSObject <NSLocking> { - (void)wait; // 等待 - (BOOL)waitUntilDate:(NSDate *)limit; // 等待某一個時間段 - (void)signal; // 喚醒 - (void)broadcast; // 喚醒所有睡眠執行緒 }
以上可以檢視 pthread_mutex
使用。
atomic
atomic
用於保證屬性 setter
和 getter
的原子性操作,相當於對 setter
和 getter
內部加了同步鎖。它並不能保證使用屬性的使用過程是執行緒安全的。
NSConditionLock
NSConditionLock
是對 NSCondition
的進一步封裝。可以設定具體的條件值。
// 遵循NSLocking協議。 @interface NSConditionLock : NSObject <NSLocking> { - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER; // 初始化,傳入一個條件值 @property (readonly) NSInteger condition; - (void)lockWhenCondition:(NSInteger)condition; // 條件值符合加鎖 - (BOOL)tryLock; //嘗試加鎖 - (BOOL)tryLockWhenCondition:(NSInteger)condition; - (void)unlockWithCondition:(NSInteger)condition; - (BOOL)lockBeforeDate:(NSDate *)limit; - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; @end
示例程式碼:
// 刪除 - (void)__one { // 當鎖內部條件值為1時,加鎖。 //[self.condition lockWhenCondition:1]; [self.condition lock]; // 直接使用lock也可以 sleep(1); NSLog(@"%s ①", __func__); [self.condition unlockWithCondition:2]; // 解鎖,並且條件設定為2 } // 新增 - (void)__two { [self.condition lockWhenCondition:2]; //條件值為2時,加鎖。 sleep(1); NSLog(@"%s ②", __func__); [self.condition unlockWithCondition:3]; } // 新增 - (void)__three { [self.condition lockWhenCondition:3]; //條件值為2時,加鎖。 sleep(1); NSLog(@"%s ③", __func__); [self.condition unlock]; } - (void)otherTest { // ① [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start]; // ② [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start]; // ③ [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start]; // 通過設定條件值,可以決定執行緒的執行順序。 }
輸出結果:
one] ①
two] ②-[LENSConditionLock __three] ③
訊號量
常用API:
// 初始化 dispatch_semaphore_t semaphore = dispatch_semaphore_create(5); // 如果訊號量的值<=0,當前執行緒就會進入休眠等待,直到訊號量的值>0 // 如果訊號量的值>0,就減1,然後往下執行後面的程式碼 dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER); // 讓訊號量的值增加1,訊號量值不等於零時,前面的等待的程式碼會執行。 dispatch_semaphore_signal(self.semaphore);
dispatch_semaphore
訊號量的初始值,控制執行緒的最大併發訪問數量。
訊號量的初始值為1,代表同時只允許1條執行緒訪問資源,保證執行緒同步。
示例程式碼:
// 設定訊號量初始值為5。 - (void)otherTest { for (int i = 0; i < 20; i ++) { [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start]; } } - (void)test { // 如果訊號量的值<=0,當前執行緒就會進入休眠等待,直到訊號量的值>0 // 如果訊號量的值>0,就減1,然後往下執行後面的程式碼 dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER); sleep(2); NSLog(@"test - %@", [NSThread currentThread]); // 讓訊號量的值增加1 dispatch_semaphore_signal(self.semaphore); }
@synchronized
@synchronized
是對 mutex
遞迴鎖的封裝。
不推薦使用,效能比較差。
// 原始碼:objc4中的objc-sync.mm @synchronized (obj) { }
效能比較
不再安全的OSSpinLock 中對比了不同鎖的效能。
推薦使用 dispatch_semaphore
和 pthread_mutex
兩個。因為 OSSpinLock
效能最好但是不安全, os_unfair_lock
在iOS10才出現低版本不支援不推薦。
自旋鎖、互斥鎖的選擇
自旋鎖預計執行緒等待鎖的時間很短,加鎖經常被呼叫但競爭情況很少出現。常用於多核處理器。
互斥鎖預計等待鎖的時間較長,單核處理器。臨界區有IO操作,例如檔案讀寫。
小結
- 怎樣用GCD實現多讀單寫?
- iOS提供幾種多執行緒技術各自的特點?
- NSOperation物件在Finished之後是怎樣從佇列中移除的?
- 你都用過哪些鎖?結合實際談談你是怎樣使用的?