線程安全(二)

原文連接html

以前寫過一篇線程安全,簡單介紹了保護數據安全的多種方式,以及其中一部分方式的原理。基於此基礎,本文將介紹如何避免鎖的性能浪費,以及如何實現無鎖安全結構緩存

避免鎖的性能浪費

爲了不多個線程對數據的破壞,在使用鎖保障線程安全的狀況下,存在幾個影響鎖性能的重要因素:安全

  • 上下文切換
  • 臨界區資源耗時

若是可以減小這些因素的損耗,就能有效的提升鎖的性能多線程

自旋鎖

一般來講,當一個線程獲取鎖失敗後,會被添加到一個等待隊列的末尾,而後休眠。直到鎖被釋放後,依次喚醒訪問臨界資源。休眠時會發生線程的上下文切換,當前線程的寄存器信息會被保存到磁盤上,考慮到這些狀況,能作的有兩點:函數

  1. 換一個更快的磁盤
  2. 改用自旋鎖

自旋鎖採用死循環等待鎖釋放來替代線程的休眠和喚醒,避免了上下文切換的代價。當臨界的代碼足夠短,使用自旋鎖對於性能的提高是立竿見影的工具

鎖粒度

粒度是指顆粒的大小性能

對於鎖來講,鎖的粒度大小取決於鎖保護的臨界區的大小。鎖的粒度越小,臨界區的操做就越小,反之亦然,因爲臨界區執行代碼時間致使的損耗問題我稱做粒度鎖問題。舉個例子,假如某個修改元素的方法包括三個操做:查找緩存->查找容器->修改元素atom

- (void)modifyName: (NSString *)name withId: (id)usrId {
    lock();
    User *usr = [self.caches findUsr: usrId];
    if (!usr) {
        usr = [self.collection findUsr: usrId];
    }
    if (!usr) {
        unlock();
        return;
    }
    usr.name = name;
    unlock();
}
複製代碼

實際上整個修改操做之中,只有最後的修改元素存在安全問題須要加鎖,而且若是加鎖的臨界區代碼執行時間過長可能致使有更多的線程須要等待鎖,增長了鎖使用的損耗。所以加鎖代碼應當儘可能的短小簡單:spa

- (void)modifyName: (NSString *)name withId: (id)usrId {
    User *usr = [self.caches findUsr: usrId];
    if (!usr) {
        usr = [self.collection findUsr: usrId];
    }
    if (!usr) {
        return;
    }
    
    lock();
    usr.name = name;
    unlock();
}
複製代碼

大段代碼改成小段代碼加鎖是一種常見的減小鎖性能損耗的作法,所以再也不多提。但接下來要說的是另外一種常見但由於鎖粒度形成損耗的問題:設想一下這個場景,在改良後的代碼使用中,線程A對第三個元素進行修改,線程B對第四個元素進行修改:線程

在兩個線程修改user的過程當中,實際上雙方的操做是不衝突,可是線程B必須等待A完成修改工做,形成這個現象的緣由是雖然看起來是對usr.name進行了加鎖,但其實是鎖住了collectioncaches的操做,因此避免這種隱藏的粒度鎖問題的方案是以容器元素單位構建鎖:包括全局鎖獨立鎖兩種:

  • 全局鎖

    構建一個global lock的集合,用hash的手段爲修改元素對應一個鎖:

    id l = [SLGlobalLocks getLock: hash(usr)];
      lock(l);
      usr.name = name;
      unlock(l);
    複製代碼

    使用全局鎖的好處包括能夠在設計上能夠懶加載生成鎖,限制bucketCount來避免建立過多的鎖實例,基於hash的映射關係,鎖能夠被多個對象獲取,提升複用率。但缺點也是明顯的,匹配鎖的額外損耗,hash映射可能致使多個鎖圍觀一個鎖工做等。事實上@synchronized就是已存在的全局鎖方案

  • 獨立鎖

    這個方案的名字是隨便起的,從設計上要求容器的每一個元素擁有本身的獨立鎖:

    @interface SLLockItem: NSObject
      
      @property (nonatomic, strong) id item;
      @property (nonatomic, strong) NSLock *lock;
      
      @end
      
      SLLockItem *item = [self.collection findUser: usrId];
      [item.lock lock];
      User *usr = item.item;
      usr.name = name;
      [item.lock unlock];
    複製代碼

    獨立鎖保證了不一樣元素之間的加鎖是絕對獨立的,粒度徹底可控,但鎖難以複用,容器越長,須要建立的鎖實例就越多也是致命的缺點。而且在鏈式結構中,增刪操做的加鎖是要和previous節點的修改操做發生競爭的,在實現上更加麻煩

無鎖安全結構

無鎖化是徹底拋棄加鎖工具,實現多線程訪問安全的方案。無鎖化須要去分解操做流程,找出真正須要保證安全的操做,舉個例子:存在鏈表A -> B -> C,刪除B的代碼以下:

Node *cur = list;
while (cur.next != B && cur.next != nil) {
    cur = cur.next;
}

if (cur.next == nil) {
    return;
}

cur.next = cur.next.next;
複製代碼

只要A.next的修改是不受多線程干擾的,那麼就能保證刪除元素的安全

CAS

compare and swap是計算機硬件提供的一種原子操做,它會比較兩個值是否相等,而後決定下一步的執行指令,iOS對於這種操做的支持須要導入<libkern/OSAtomic.h>文件。

bool	OSAtomicCompareAndSwapPtrBarrier( void *oldVal, void *newVal, void * volatile *theVal )
複製代碼

函數會在oldValtheVal相同的狀況下將oldVal存儲的值修改成newVal,所以刪除B的代碼只要保證在A.next變成nil以前是一致的就能夠保證安全:

Node *cur = list;
while (cur.next != B && cur.next != nil) {
    cur = cur.next;
}

if (cur.next == nil) {
    return;
}

while (true) {
    void *oldValue = cur.next;
    if (OSAtomicCompareAndSwapPtrBarrier(oldValue, nil, &cur.next)) {
        break;
    } else {
        continue;
    } 
}
複製代碼

基於上面刪除B的例子,同一時間存在其餘線程在A節點後追加D節點:

因爲CPU可能在任務執行過程當中切換線程,若是D節點的修改工做正好在刪除任務的中間完成,最終可能致使的是D節點的誤刪:

因此上面的CAS還須要考慮A.next是否發生了改變:

Node *cur = list;
while (cur.next.value != B && cur.next != nil) {
    cur = cur.next;
}

if (cur.next == nil) {
    return;
}

while (true) {
    void *oldValue = cur.next;
    
    // next已經再也不指向B
    if (!OSAtomicCompareAndSwapPtrBarrier(B, B, &cur.next.value)) {
        break;
    }
    
    if (OSAtomicCompareAndSwapPtrBarrier(oldValue, nil, &cur.next)) {
        break;
    } else {
        continue;
    } 
}
複製代碼

題外話

OSAtomicCompareAndSwapPtrBarrier除了保證修改操做的原子性,還帶有barrier的做用。在如今CPU的設計上,會考慮打亂代碼的執行順序來獲取更快的執行速度,好比說:

/// 線程1執行
A.next = D;
D.next = C;

/// 線程2執行
while (D.next != C) {}
NSLog(@"%@", A.next);
複製代碼

因爲執行順序會被打亂,執行的時候變成:

D.next = C;
A.next = D;

while (D.next != C) {}
NSLog(@"%@", A.next);
複製代碼

輸出的結果可能並非D,而只要在D.next = C前面插入一句barrier函數,就能保證在這句代碼前的指令不會被打亂執行,保證正確的代碼順序

最後

很方,這個月想了不少想寫的內容,而後發現別人都寫過,尷尬的一筆。果真仍是本身太鶸了,最後隨便趕工了一篇全是水貨的文章,瑟瑟發抖

關注個人公衆號獲取更新信息
相關文章
相關標籤/搜索