iOS知識梳理 - 多線程(3)鎖

多線程模型下,由於共享內存帶來的衝突風險,鎖是個避不開的話題。

關於鎖

首先從平臺無關的角度看,從能力上區分,主要有以下幾種鎖:

  1. 互斥鎖(mutex):最普通的鎖,阻塞等待,一種二元鎖機制,只允許一個線程進入臨界區
  2. 自旋鎖(spin lock):能力上跟互斥鎖一樣的,只是它是忙等待
  3. 信號量(semaphore):信號量可以理解爲互斥鎖的推廣形態,即互斥鎖取值0/1,而信號量可以取更多的值,從而應對更復雜的同步。
  4. 條件鎖(condition lock):有時候互斥的條件是複雜的而不是簡單的數量上的競爭,此時可以用條件鎖,條件鎖的加鎖解鎖是通過代碼觸發的。
  5. 讀寫鎖(read-write lock):顧名思義,就像文件讀寫,讀操作之間並不互斥,但寫操作與任何操作互斥。
  6. 遞歸鎖(recursivelock):互斥鎖的一個特例,允許同一個線程在未釋放其擁有的鎖時反覆對該鎖進行加鎖操作。

所有的鎖,語義上基本就這幾種,

iOS中的鎖

以API提供者的維度梳理一下iOS的鎖

  1. 內核

    1. OSSpinLock:內核提供的自旋鎖,已廢棄
    2. os_unfair_lock:iOS10後官方推薦用來替代OSSpinLock的方案,性能很好
  2. pthread:POSIX標準真的是大而全...啥都有

    1. pthread_mutex:pthread的互斥鎖
    2. pthread_rwlock:pthread的讀寫鎖
    3. pthread_cond_t :pthread的條件鎖
    4. sem_t:pthread的信號量
    5. pthread_spin_lock:pthread的自旋鎖
  3. GCD

    1. dispatch_semaphore:gcd的信號量
  4. Cocoa Foundation

    1. NSLock:CF的互斥鎖
    2. NSCondition:條件變量
    3. NSConditionLock:條件鎖,在條件變量之上做了封裝
    4. NSRecursiveLock
  5. objc runtime

    1. synchronized:本質上是pthread_mutex的上層封裝,參考這裏

以上,相對全面地列舉了iOS中的鎖,它們是不同層級的庫提供的,但由於iOS中所有的線程本質上都是內核級線程,因此這些鎖是能夠公用的。

  1. 串行隊列
  2. dispatch_barrier_async:柵欄函數,隔離前後的任務
  3. atomic

性能對比

環境:iPhone 7 plus + iOS 11

基於YY老師 不再安全的 OSSpinLock 中的性能對比代碼,加入了os_unfair_lock,重新跑的一個性能對比。測試代碼在這裏
lock_benchmark.png

上面測試的是純粹的加鎖解鎖性能,中間沒有任何邏輯也不存在多線程搶佔,爲了更貼合我們的實際環境,我構造了一個簡單的多線程環境:NSOperationQueue最大併發數爲10,創建10個NSOperation,每個NSOperation做10w次i++操作,每次操作加鎖,代碼在這裏,結果如下:

lock_benchmark2.png

可以看到多線程搶佔的情形下結果跟前面略有不同,在真實業務場景下這個數據應該更有參考意義。

如何選擇

由於OSSpinLock存在的優先級反轉問題,已經廢棄不再使用。(參考:不再安全的 OSSpinLock

  1. 一般場景,直接用@synchronized。使用最方便。一般業務開發場景,鎖的性能影響不大,能力上也只需要簡單的互斥鎖,因此怎麼方便怎麼來。而且@synchronized性能也沒有差太多。
  2. 性能苛刻的場景:os_unfair_lock,自旋鎖廢棄後官方推薦的替代品,性能優異。
  3. 需要信號量:dispatch_semaphore
  4. 需要條件鎖:NSCondition
  5. 需要讀寫鎖:pthread_rwlock
  6. 需要遞歸鎖: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;
}

推薦閱讀

  1. 互斥鎖,同步鎖,臨界區,互斥量,信號量,自旋鎖之間聯繫是什麼? - Tim Chen的回答 - 知乎
  2. 互斥鎖,同步鎖,臨界區,互斥量,信號量,自旋鎖之間聯繫是什麼? - 胖君的回答 - 知乎
  3. 不再安全的 OSSpinLock
  4. iOS多線程安全-13種線程鎖
  5. iOS開發中的11種鎖以及性能對比 )
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章