多線程模型下,由於共享內存帶來的衝突風險,鎖是個避不開的話題。
關於鎖
首先從平臺無關的角度看,從能力上區分,主要有以下幾種鎖:
- 互斥鎖(mutex):最普通的鎖,阻塞等待,一種二元鎖機制,只允許一個線程進入臨界區
- 自旋鎖(spin lock):能力上跟互斥鎖一樣的,只是它是忙等待
- 信號量(semaphore):信號量可以理解爲互斥鎖的推廣形態,即互斥鎖取值0/1,而信號量可以取更多的值,從而應對更復雜的同步。
- 條件鎖(condition lock):有時候互斥的條件是複雜的而不是簡單的數量上的競爭,此時可以用條件鎖,條件鎖的加鎖解鎖是通過代碼觸發的。
- 讀寫鎖(read-write lock):顧名思義,就像文件讀寫,讀操作之間並不互斥,但寫操作與任何操作互斥。
- 遞歸鎖(recursivelock):互斥鎖的一個特例,允許同一個線程在未釋放其擁有的鎖時反覆對該鎖進行加鎖操作。
所有的鎖,語義上基本就這幾種,
iOS中的鎖
以API提供者的維度梳理一下iOS的鎖
-
內核
- OSSpinLock:內核提供的自旋鎖,已廢棄
- os_unfair_lock:iOS10後官方推薦用來替代OSSpinLock的方案,性能很好
-
pthread:POSIX標準真的是大而全...啥都有
- pthread_mutex:pthread的互斥鎖
- pthread_rwlock:pthread的讀寫鎖
- pthread_cond_t :pthread的條件鎖
- sem_t:pthread的信號量
- pthread_spin_lock:pthread的自旋鎖
-
GCD
- dispatch_semaphore:gcd的信號量
-
Cocoa Foundation
- NSLock:CF的互斥鎖
- NSCondition:條件變量
- NSConditionLock:條件鎖,在條件變量之上做了封裝
- NSRecursiveLock
-
objc runtime
- synchronized:本質上是pthread_mutex的上層封裝,參考這裏
以上,相對全面地列舉了iOS中的鎖,它們是不同層級的庫提供的,但由於iOS中所有的線程本質上都是內核級線程,因此這些鎖是能夠公用的。
- 串行隊列
- dispatch_barrier_async:柵欄函數,隔離前後的任務
- atomic
性能對比
環境:iPhone 7 plus + iOS 11
基於YY老師 不再安全的 OSSpinLock 中的性能對比代碼,加入了os_unfair_lock
,重新跑的一個性能對比。測試代碼在這裏
上面測試的是純粹的加鎖解鎖性能,中間沒有任何邏輯也不存在多線程搶佔,爲了更貼合我們的實際環境,我構造了一個簡單的多線程環境:NSOperationQueue
最大併發數爲10,創建10個NSOperation
,每個NSOperation
做10w次i++操作,每次操作加鎖,代碼在這裏,結果如下:
可以看到多線程搶佔的情形下結果跟前面略有不同,在真實業務場景下這個數據應該更有參考意義。
如何選擇
由於OSSpinLock存在的優先級反轉問題,已經廢棄不再使用。(參考:不再安全的 OSSpinLock )
- 一般場景,直接用
@synchronized
。使用最方便。一般業務開發場景,鎖的性能影響不大,能力上也只需要簡單的互斥鎖,因此怎麼方便怎麼來。而且@synchronized
性能也沒有差太多。 - 性能苛刻的場景:
os_unfair_lock
,自旋鎖廢棄後官方推薦的替代品,性能優異。 - 需要信號量:
dispatch_semaphore
- 需要條件鎖:
NSCondition
- 需要讀寫鎖:
pthread_rwlock
- 需要遞歸鎖:
NSRecursiveLock
使用
1. 自旋鎖 OSSpinLock
自旋鎖是這些鎖中唯一一個依靠忙等待實現的鎖,也就是說可以理解成一個暴力的while循環,因此會浪費較多的CPU,但它是所有鎖中性能最高的。適用於對時延要求比較苛刻、臨界區計算量比較小、本身CPU不存在瓶頸的場景。
但是現在不能用了。YY老師在不再安全的 OSSpinLock 中講得很清楚了,當低優先級的線程已進入臨界區,高優先級的線程想要獲取資源就需要忙等待,佔用大量CPU,導致低優先級線程遲遲不能執行完臨界區代碼,導致類死鎖的問題。
OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
// do something
OSSpinLockUnlock(&lock);
2. os_unfair_lock
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&lock);
// do something
os_unfair_lock_unlock(&lock);
3. pthread_mutex
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);
// do something
pthread_mutex_unlock(&lock);
4. dispatch_semaphore
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
// do something
dispatch_semaphore_signal(lock);
dispatch_semaphore_create
傳入參數是信號量的值,在這裏就是能夠同時進入臨界區的線程數。dispatch_semaphore_wait
,當信號量大於0時減一併進入臨界區,如果信號量等於0則等待直到信號量不爲0或到達設定時間。dispatch_semaphore_signal
使信號量加1。
5. NSLock
NSLock *lock = [NSLock new];
[lock lock];
// do something
[lock unlock];
6. NSCondition
條件鎖,以生產者消費者模型爲例
NSCondition *condition = [NSCondition new];
// Thread 1: 消費者
- (void)consumer
{
[condition lock];
while(conditionNotSatisfied){
[condition wait]
}
// 消費邏輯
consume();
[condition unlock];
}
// Thread 2: 生產者
- (void)producer
{
[condition lock];
// 生產邏輯
produce();
[condition signal];
[condition unlock];
}
7. NSConditionLock
條件鎖,跟NSCondition差不多,對條件做了封裝,簡化了使用但也沒NSCondition那麼靈活了。
NSConditionLock *condition = [[NSConditionLock alloc] initWithCondition:1];
// Thread 1: 消費者
- (void)consumer
{
[condition lockWhenCondition:1];
while(conditionNotSatisfied){
[condition wait]
}
// 消費邏輯
consume();
[condition unlockWithCondition:0];
}
// Thread 2: 生產者
- (void)producer
{
[condition lock];
// 生產邏輯
produce();
[condition unlockWithCondition:1];
}
8. NSRecursiveLock
可以遞歸調用的互斥鎖。
int i = 0;
NSRecursiveLock *lock = [NSRecursiveLock new];
- (void)testLock
{
if(i > 0){
[lock lock];
[self testLock];
i --;
[lock lock];
}
}
9. @synchronized
普通的鎖,用着方便。
@synchronized(self) {
// do something
}
10. pthread_rwlock
讀寫鎖,一般也不怎麼用得上,這裏給了個字典set/get的例子,但是實際業務場景,通常普通的互斥鎖就可以了。
在讀操作比寫操作多很多的情況下,讀寫鎖的收益比較可觀。
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
NSMutableDictionary *dic = [NSMutableDictionary new];
- (void)set
{
// 寫模式加鎖
pthread_rwlock_wrlock(&lock);
dic[@"key"] = @"value";
// 解鎖
pthread_rwlock_unlock(&lock);
}
- (NSString *)get
{
NSString *value;
// 寫模式加鎖
pthread_rwlock_rdlock(&lock);
value = dic[@"key"];
// 解鎖
pthread_rwlock_unlock(&lock);
return value;
}
推薦閱讀