原文連接html
以前寫過一篇線程安全,簡單介紹了保護數據安全的多種方式,以及其中一部分方式的原理。基於此基礎,本文將介紹如何避免鎖的性能浪費,以及如何實現無鎖安全結構緩存
爲了不多個線程對數據的破壞,在使用鎖保障線程安全的狀況下,存在幾個影響鎖性能的重要因素:安全
若是可以減小這些因素的損耗,就能有效的提升鎖的性能多線程
一般來講,當一個線程獲取鎖失敗後,會被添加到一個等待隊列
的末尾,而後休眠。直到鎖被釋放後,依次喚醒訪問臨界資源。休眠時會發生線程的上下文切換,當前線程的寄存器信息會被保存到磁盤上,考慮到這些狀況,能作的有兩點:函數
自旋鎖
自旋鎖
採用死循環等待鎖釋放來替代線程的休眠和喚醒,避免了上下文切換的代價。當臨界的代碼足夠短,使用自旋鎖
對於性能的提高是立竿見影的工具
粒度是指顆粒的大小性能
對於鎖來講,鎖的粒度大小取決於鎖保護的臨界區的大小。鎖的粒度越小,臨界區的操做就越小,反之亦然,因爲臨界區執行代碼時間致使的損耗問題我稱做粒度鎖問題
。舉個例子,假如某個修改元素的方法包括三個操做:查找緩存
->查找容器
->修改元素
: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
進行了加鎖,但其實是鎖住了collection
或caches
的操做,因此避免這種隱藏的粒度鎖問題的方案是以容器元素單位構建鎖:包括全局鎖
和獨立鎖
兩種:
全局鎖
構建一個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
的修改是不受多線程干擾的,那麼就能保證刪除元素的安全
compare and swap
是計算機硬件提供的一種原子操做,它會比較兩個值是否相等,而後決定下一步的執行指令,iOS
對於這種操做的支持須要導入<libkern/OSAtomic.h>
文件。
bool OSAtomicCompareAndSwapPtrBarrier( void *oldVal, void *newVal, void * volatile *theVal )
複製代碼
函數會在oldVal
和theVal
相同的狀況下將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
函數,就能保證在這句代碼前的指令不會被打亂執行,保證正確的代碼順序
很方,這個月想了不少想寫的內容,而後發現別人都寫過,尷尬的一筆。果真仍是本身太鶸了,最後隨便趕工了一篇全是水貨的文章,瑟瑟發抖