鎖的分類
自旋鎖
線程反復檢查鎖變量是否可用。由于線程在這一過程中保持執(zhí)行, 因此是一種忙等待。一旦獲取了自旋鎖,線程會一直保持該鎖,直至顯式釋放自旋鎖。 自旋鎖避免了進程上下文的調(diào)度開銷,因此對于線程只會阻塞很短時間的場合是有效的。
互斥鎖
是一種用于多線程編程中,防止兩條線程同時對同一公共資源(比如全局變量)進行讀寫的機制。該目的通過將代碼切片成一個一個的臨界區(qū),而達成。這里有兩個要注意的點互斥跟同步,互斥就是當多個線程進行同一操作的時候,同一時間只有一個線程可以進行操作。同步是多個線程進行同一操作的時候,按照相應的順序執(zhí)行?;コ怄i又分為兩種情況,可遞歸和不可遞歸。
這里屬于互斥鎖的有:
NSLockpthread_mutex@synchronized
條件鎖
就是條件變量,當進程的某些資源要求不滿足時就進入休眠,也就
是鎖住了。當資源被分配到了,條件鎖打開,進程繼續(xù)運行。
NSConditionNSConditionLock
遞歸鎖
就是同一個線程可以加鎖N次而不會引發(fā)死鎖。
NSRecursiveLockpthread_mutex(recursive)
信號量
信號量(
semaphore):是一種更高級的同步機制,互斥鎖可以說是semaphore在僅取值 0/1 時的特例。信號量可以有更多的取值空間,用來實現(xiàn)更加復雜的同步,而不單單是線程間互斥。
dispatch_semaphore
讀寫鎖
讀寫鎖實際是一種特殊的互斥鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源 進行讀訪問,寫者則需要對共享資源進行寫操作。這種鎖相對于自旋鎖而言,能提高并發(fā)性,因為在多處理器系統(tǒng)中,它允許同時有多個讀者來訪問共享資源,最大可能的讀者數(shù)為實際的邏輯CPU 數(shù)。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數(shù)相關(guān)),但不能同時既有讀者又有寫者。在讀寫鎖保持期間也是搶占失效的。
如果讀寫鎖當前沒有讀者,也沒有寫者,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里, 直到?jīng)]有任何寫者或讀者。如果讀寫鎖沒有寫者,那么讀者可以立即獲得該讀寫鎖,否則讀者必須自旋在那里,直到寫者釋放該讀寫鎖。
一次只有一個線程可以占有寫模式的讀寫鎖, 但是可以有多個線程同時占有讀模式的讀寫鎖。 正是因為這個特性,當讀寫鎖是寫加鎖狀態(tài)時, 在這個鎖被解鎖之前, 所有試圖對這個鎖加鎖的線程都會被阻塞。當讀寫鎖在讀加鎖狀態(tài)時, 所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權(quán), 但是如果線程希望以寫模式對此鎖進行加鎖,它必須直到所有的線程釋放鎖。通常,當讀寫鎖處于讀模式鎖住狀態(tài)時,如果有另外線程試圖以寫模式加鎖,讀寫鎖通常會阻塞隨后的讀模式鎖請求,這樣可以避免讀模式鎖?期占用,而等待的寫模式鎖請求?期阻塞。讀寫鎖適合于對數(shù)據(jù)結(jié)構(gòu)的讀次數(shù)比寫次數(shù)多得多的情況。因為,讀模式鎖定時可以共享,以寫模式鎖住時意味著獨占,所以讀寫鎖又叫共享-獨占鎖。
總結(jié)
其實基本的鎖就包括了三類,自旋鎖, 互斥鎖 讀寫鎖,其他的比如條件鎖,遞歸鎖,信號量都是上層的封裝和實現(xiàn)!
pthread
在 Posix Thread 中定義有一套專?用于線程同步的函數(shù) mutex,用于保證在任何時刻,都只能有一個線程訪問該對象。 當獲取鎖操作失敗時,線程會進入睡眠,等待鎖釋放時被喚醒。
創(chuàng)建和銷毀
A:POSIX定義了一個宏PTHREAD_MUTEX_INITIALIZER來靜態(tài)初始化互斥鎖
B:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
C:pthread_mutex_destroy ()用于注銷一個互斥鎖鎖操作
int pthread_mutex_lock(pthread_mutex_t *mutex)int pthread_mutex_unlock(pthread_mutex_t *mutex)int pthread_mutex_trylock(pthread_mutex_t *mutex)-
pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經(jīng)被占據(jù)時返回EBUSY而不是掛起等待。
NSLock 和 NSReLock 的分析
這里我們通過幾個使用案例來介紹一下 NSLock 跟 NSReLock 這兩種鎖。
-
案例 1
類似這樣一段代碼,當我們不加鎖的情況下打印就會亂序,當我們在 testMethod(10) 執(zhí)行前后分別加鎖解鎖就會循環(huán)按順序打印。
-
案例 2
類似這種,我們把 lock 跟 unlock 調(diào)整了下位置,就會出現(xiàn)類似死鎖的現(xiàn)象,testMethod 遞歸執(zhí)行。導致這個的原因是因為 NSLock 不具有可遞歸性。針對這種情況我們可以用 @synchronized 來解決,也可以用 NSRecursiveLock 來解決。因為在前面已經(jīng)分析了 @synchronized,這里我們來試一下用 NSRecursiveLock 來解決,NSRecursiveLock 的使用頻率也很高,我們在很多三方庫里面在一些遞歸加鎖的場景也可以看到 NSRecursiveLock 的應用。
-
案例 3
當我們使用 NSRecursiveLock 的時候發(fā)現(xiàn)第一次可以打印,但是第二次就報錯了,這是因為 NSRecursiveLock 具有可遞歸性,但是不支持多線程執(zhí)行。
- 案例 4

我們使用 @synchronized 既解決了遞歸調(diào)用,也解決了多線程的問題。
NSCondtion的分析
NSCondition的對象實際上作為一個鎖和一個線程檢查器,鎖主要為了當檢測條件時保護數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務;線程檢查器主要是根據(jù)條件決定是否繼續(xù)運行線程,即線程是否被阻塞。
NSCondition 的 api介紹:
-
[condition lock]:一般用于多線程同時訪問、修改同一個數(shù)據(jù)源,保證在同一 時間內(nèi)數(shù)據(jù)源只被訪問、修改一次,其他線程的命令需要在lock外等待,只到unlock,才可訪問。 -
[condition unlock]:與lock 同時使用。 -
[condition wait]:讓當前線程處于等待狀態(tài)。 -
[condition signal]:CPU發(fā)信號告訴線程不用在等待,可以繼續(xù)執(zhí)行。
案例
- (void)cx_testConditon{
_testCondition = [[NSCondition alloc] init];
//創(chuàng)建生產(chǎn)-消費者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self cx_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self cx_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self cx_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self cx_consumer];
});
}
}
- (void)cx_producer {
[_testCondition lock]; // 操作的多線程影響
self.ticketCount = self.ticketCount + 1;
NSLog(@"生產(chǎn)一個 現(xiàn)有 count %zd",self.ticketCount);
if (self.ticketCount > 0) {
[_testCondition signal]; // 信號
}
[_testCondition unlock];
}
- (void)cx_consumer {
[_testCondition lock]; // 操作的多線程影響
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait];
}
//注意消費行為,要在等待條件判斷之后
self.ticketCount -= 1;
NSLog(@"消費一個 還剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}

類似這樣一段代碼,我們定義了生產(chǎn)方法 cx_producer 跟消費方法 cx_consumer,在 ticketCount 值修改的時候都會加鎖,但是在消費方法里面會判斷 ticketCount 小于零的時候就會進入等待,禁止消費,在生產(chǎn)方法 cx_producer 中判斷 ticketCount 大于零的時候就會發(fā)送信號,繼續(xù)執(zhí)行。保證了事務的安全。
foundation 源碼關(guān)于鎖的封裝
我們前面也講了,例如 NSLock, NSRecursiveLock, NSCondition 等這些鎖的底層都是基于 pthread 的封裝,但是這些鎖的底層都是在 NSFoundation 框架下實現(xiàn)的,但是 NSFoundation 框架并不開源,我們怎么來探究它們的底層實現(xiàn)呢?這里我們?nèi)×藗€巧,用 swift 的 foundation 框架源碼來進行探究。源碼已上傳到 github,感興趣的小伙伴可以下載。
NSLock

在我們的代碼下我們我們按住 control + command 鍵點擊進入 NSLock 的頭文件實現(xiàn)可以看到 NSLock 是繼承于 NSObject 的一個類,只是遵循了 NSLocking 協(xié)議。因為這里只能看到協(xié)議的聲明,具體實現(xiàn)我們打開源碼來看一下。


NSRecursiveLock

上面案例分析的時候我們知道 NSRecursiveLock 相對于 NSLock 具有可遞歸性,對比他們的源碼我們可以看到,只是因為 NSRecursiveLock 的底層 pthread_mutex_init 的時候多了一個 attrs 參數(shù)。它們的 lock 與 unlock 方法的底層實現(xiàn)都是一樣。
NSCondition

查看 NSCondition 的源碼實現(xiàn),我們發(fā)現(xiàn) NSCondition 只是在初始化的時候多了一句 pthread_cond_init(cond, nil),它的 wait 方法底層調(diào)用的是 pthread_cond_wait(cond, mutex)。通過對這幾種鎖的分析我們可以看到它們的底層都是基于 pthread 的封裝,當我們不知道使用哪種鎖的時候,用 pthread 來實現(xiàn)是最完美的。
NSConditionLock
NSConditionLock 介紹
- 1.1
NSConditionLock是一種鎖,一旦一個線程獲得鎖,其他線程一定等待。 - 1.2
[conditionLock lock]表示conditionLock期待獲得鎖,如果沒有其他線程獲得鎖(不需要判斷內(nèi)部的condition) 那它能執(zhí)行此行以下代碼,如果已經(jīng)有其他線程獲得鎖(可能是條件鎖,或者無條件鎖),則等待,直至其他線程解鎖。 - 1.3
[conditionLock lockWhenCondition:A條件]表示如果沒有其他線程獲得該鎖,但是該鎖內(nèi)部的condition不等于A條件,它依然不能獲得鎖,仍然等待。如果內(nèi)部的condition等于A條件,并且 沒有其他線程獲得該鎖,則進入代碼區(qū),同時設(shè)置它獲得該鎖,其他任何線程都將等待它代碼的 完成,直至它解鎖。 - 1.4
[conditionLock unlockWithCondition:A條件]表示釋放鎖,同時把內(nèi)部的condition設(shè)置為A條件。 - 1.5
return = [conditionLock lockWhenCondition:A條件 beforeDate:A時間]表示如果被鎖定(沒獲得鎖),并超過該時間則不再阻塞線程。但是注意:返回的值是NO,它沒有改變鎖的狀態(tài),這個函數(shù)的目的在于可以實現(xiàn)兩種狀態(tài)下的處理。 - 1.6 所謂的
condition就是整數(shù),內(nèi)部通過整數(shù)比較條件。
案例
- (void)cx_testConditonLock{
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"線程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
sleep(0.1);
NSLog(@"線程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"線程 3");
[conditionLock unlock];
});
}
線程 1 調(diào)用
[NSConditionLock lockWhenCondition:],此時此刻因為不滿足當前條件,所以會進入waiting狀態(tài),當前進入到waiting時,會釋放當前的互斥鎖。此時當前的線程 3 調(diào)用
[NSConditionLock lock:],本質(zhì)上是調(diào)用[NSConditionLock lockBeforeDate:],這里不需要比對條件值,所以線程 3 會打印接下來線程 2 執(zhí)行
[NSConditionLock lockWhenCondition:],因為滿足條件值,所以線程 2 會打印,打印完成后會調(diào)用[NSConditionLock unlockWithCondition:],這個時候?qū)?value設(shè)置為 1,并發(fā)送boradcast, 此時線程 1 接收到當前的信號,喚醒執(zhí)行并打印。自此當前打印為 線程 3->線程 2 -> 線程 1。
[NSConditionLock lockWhenCondition:]:這里會根據(jù)傳入的condition值和Value值進行對比,如果不相等,這里就會阻塞,進入線程池,否則的話就繼續(xù)代碼執(zhí)行。[NSConditionLock unlockWithCondition:]:這里會先更改當前的value值,然后進行廣播,喚醒當前的線程。
NSConditionLock 執(zhí)行流程分析
通過上面的案例我們會有幾個疑問:
-
NSConditionLock與NSCondition有什么區(qū)別 - 初始化的時候
[[NSConditionLock alloc] initWithCondition:2]會傳入一個參數(shù) 2,這個值的作用是干什么的 -
lockWhenCondition是如何控制流程的 -
unlockWithCondition又做了什么
前面幾種鎖我們都是通過源碼看到了底層的實現(xiàn),但是當我們沒有源碼的時候我們又應該用哪種思路去分析呢?這里我們嘗試一下通過反匯編跟來探索一下。這里環(huán)境用的是真機。
-
initWithCondition流程追蹤
首先對 initWithCondition 方法下符號斷點 -[NSConditionLock initWithCondition:],這里需要注意因為是對象方法,所以符號斷點有點特殊。

斷點之后我們可以看到匯編代碼,這里 x0, x1, x2 分別代表方法的調(diào)用者, 調(diào)用方法, 參數(shù)。這里我們輸出之后跟我們 OC 代碼的調(diào)用都能一一對應上。這里我們重點追蹤 bl 的執(zhí)行,因為 bl 代表跳轉(zhuǎn)。下面我們就一步一步的斷點 bl 的執(zhí)行。

這里 x0 輸出暫時看不到,但是可以看到調(diào)用了 init 方法,并且參數(shù)是 2。

這里追蹤到 NSConditionLock 調(diào)用了 init 方法,并且參數(shù)是 2。

這里 NSConditionLock 調(diào)用了 zone 方法,也就是內(nèi)存開辟。

這里 NSCondition 調(diào)用了 allocWithZone 方法。

這里 NSCondition 調(diào)用了 init 方法。

這里就是 return,x0 就是返回對象,打印 x0 的內(nèi)存結(jié)構(gòu),可以看到它有 NSCondition 跟 2 兩個成員變量。
-
lockWhenCondition流程追蹤


這里 NSDate 調(diào)用了 distantFuture 方法且參數(shù)為 1。

這里執(zhí)行了 waitUntilDate 方法,進行了等待。

這里 NSConditionLock 調(diào)用了 lockWhenCondition:beforeDate:,第一個參數(shù)為 1,第二個參數(shù)為 [NSDate distantFuture] 的返回值。并且在這里新增符號斷點 -[NSConditionLock lockWhenCondition:beforeDate:]。

這里會斷到 lockWhenCondition:beforeDate: 方法。


lockWhenCondition:beforeDate: 之后會再次來到 lockWhenCondition 方法,只是到了線程 4,參數(shù)變?yōu)榱?2。

線程 4 中 lockWhenCondition 之后還會來到 lockWhenCondition:beforeDate: 方法。在 bl 這里 NSCondition 調(diào)用了 lock 方法。
-
unlockWithCondition流程追蹤


這里會來到 unlockWithCondition 方法,并且也進行了加鎖操作。

這里 NSCondition 調(diào)用了 broadcast 方法。

方法結(jié)束后 NSCondition 調(diào)用了 unlock 方法。

緊接著這里會來到我們 OC 代碼中的線程一中的 lockWhenCondition:beforeDate: 方法,在這里又進行了一次解鎖操作,跟上面我們兩次加鎖一一對應上了。

執(zhí)行結(jié)束也返回了 0x0000000000000001,也就是 1。

最后執(zhí)行 OC 代碼中的線程一中的 unlockWithCondition 方法。然后又會執(zhí)行上面 unlockWithCondition 方法的匯編流程。這里 1 代表不在等待。
反匯編分析與源碼對比


通過對比我們可以看到我們反匯編分析的執(zhí)行流程與源碼邏輯一致。
GCD???????????????????????????????? 實現(xiàn)多讀單寫
????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????比如在內(nèi)存中維護一份數(shù)據(jù),有多處地方可能同時操作這塊數(shù)據(jù),怎么能保證數(shù)據(jù)庫的安全呢?這里需要滿足以下三點:
- 1.讀寫互斥
- 2.寫寫互斥
- 3.讀讀并發(fā)
- (instancetype)init {
self = [super init];
if (self) {
_currentQueue = dispatch_queue_create("chenxi", DISPATCH_QUEUE_CONCURRENT);
_dic = [NSMutableDictionary dictionary];
}
return self;
}
- (void)cx_setSafeObject:(id)object forKey:(NSString *)key {
key = [key copy];
__weak __typeof(self)weakSelf = self;
dispatch_barrier_async(_currentQueue, ^{
[weakSelf.dic setObject:object forKey:key];
});
}
- (id)cx_safeObjectForKey:(NSString *)key {
__block id temp;
__weak __typeof(self)weakSelf = self;
dispatch_sync(_currentQueue, ^{
temp = [weakSelf.dic objectForKey:key];
});
return temp;
}
首先我們要維系一個GCD隊列,最好不用全局隊列,畢竟大家都知道全局隊列遇到柵欄函數(shù)是有坑點的,這里就不分析了!
因為考慮性能, 死鎖, 堵塞的因素不考慮串行隊列,用的是自定義的并發(fā)隊列!
-
首先我們來看看讀操作:
cx_safe0bjectForKey我們考慮到多線程影響是不能用異步函數(shù)的!說明:- 線程 2 獲取:
name線程 3 獲取age。 - 如果因為異步并發(fā),導致混亂本來讀的是
name結(jié)果讀到了age - 我們允許多個任務同時進去! 但是讀操作需要同步返回,所以我們選擇:同步函數(shù)(讀讀并發(fā))
- 線程 2 獲取:
我們再來看看寫操作,在寫操作的時候?qū)?
key進行了copy,關(guān)于此處的解釋,插入一段來自參考文獻的引用:
函數(shù)調(diào)用者可以自由傳遞一個
NSMutableString的key,并且能夠在函數(shù)返回后修改它。因此我們必須對傳入的字符串使用copy操作以確保函數(shù)能夠正確地工作。如果傳入的字符串不是可變的(也就是正常的NSString類型),調(diào)用copy基本上是個空操作。
-
這里我們選擇
dispatch_barrierasync,為什么是柵欄函數(shù)而不是異步函數(shù)或者同步函數(shù),下面分析:- 柵欄函數(shù)任務:之前所有的任務執(zhí)行完畢,并且在它后面的任務開始之前,期間不會有其他的任務執(zhí)行,這樣比較好的促使寫操作一個接一個寫(寫寫互斥),不會亂!
- 為什么不是異步函數(shù)? 應該很容易分析,畢竟會產(chǎn)生混亂!
- 為什么不用同步函數(shù)?如果讀寫都操作了,那么用同步函數(shù),就有可能存在:我寫需要等待讀操作回來才能執(zhí)行,顯然這里是不合理!




