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

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

關於鎖

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

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

全部的鎖,語義上基本就這幾種,git

iOS中的鎖

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

  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 Foundationasync

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

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

以上,相對全面地列舉了iOS中的鎖,它們是不一樣層級的庫提供的,但因爲iOS中全部的線程本質上都是內核級線程,所以這些鎖是可以公用的。post

  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種鎖以及性能對比 )
相關文章
相關標籤/搜索