1. 性能對比
(來自 ibireme 的 [不再安全的 OSSpinLock][] 一文)

2. 特點分析
2.1. @synchronized(obj)
可能是我們最常用的方式,但是它的性能是最差的,??,obj是該鎖的唯一標(biāo)識,只有當(dāng)標(biāo)識相同時,才能滿足互斥需求,優(yōu)點就是我們不需要在代碼中顯式的創(chuàng)建鎖對象,便可以實現(xiàn)鎖的機(jī)制,但作為一種預(yù)防措施,@synchronized塊會隱式的添加一個異常處理例程來保護(hù)代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖。所以如果不想讓隱式的異常處理例程帶來額外的開銷,你可以考慮使用鎖對象。
2.2 dispatch_semaphore
dispatch_semaphore 是信號量,但當(dāng)信號總量設(shè)為 1 時也可以當(dāng)作鎖來。在沒有等待情況出現(xiàn)時,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現(xiàn)時,性能就會下降許多。相對于 OSSpinLock 來說,它的優(yōu)勢在于等待時不會消耗 CPU 資源。
dispatch_semaphor 和 NSCondition 類似,都是一種基于信號的同步方式,但 NSCondition 信號只能發(fā)送,不能保存(如果沒有線程在等待,則發(fā)送的信號會失效)。而 dispatch_semaphore 能保存發(fā)送的信號。dispatch_semaphore 的核心是 ispatch_semaphore_t 類型的信號量。
dispatch_semaphore_create(1)方法可以創(chuàng)建一個 dispatch_semaphore_t 類型的信號量,設(shè)定信號量的初始值為 1。注意,這里的傳入的參數(shù)必須大于或等于 0,否則 dispatch_semaphore_create 會返回 NULL。
dispatch_semaphore_wait(signal, timeout)方法會判斷 signal 的信號值是否大于 0。大于 0 不會阻塞線程,消耗掉一個信號,執(zhí)行后續(xù)任務(wù)。如果信號值為 0,該線程會和 NSCondition 一樣直接進(jìn)入 waiting 狀態(tài),等待其他線程發(fā)送信號喚醒線程去執(zhí)行后續(xù)任務(wù),或者當(dāng) timeout 時限到了,也會執(zhí)行后續(xù)任務(wù)。
dispatch_semaphore_signal(signal)發(fā)送信號,如果沒有等待的線程接受信號,則使 signal 信號值加一(做到對信號的保存)。
從上面的實例代碼可以看到,一個dispatch_semaphore_wait(signal, timeout)方法會去對應(yīng)一個 dispatch_semaphore_signal(signal)看起來像 NSLock 的 lock 和 unlock,其實可以這樣理解,區(qū)別只在于有信號量這個參數(shù),lock unlock 只能同一時間,一個線程訪問被保護(hù)的臨界區(qū),而如果 dispatch_semaphore 的信號量初始值為 x ,則可以有 x 個線程同時訪問被保護(hù)的臨界區(qū)。
2.3 NSLock
NSLock是Cocoa提供給我們最基本的鎖對象,這也是我們經(jīng)常所使用的,除lock和unlock方法外,NSLock還提供了tryLock和lockBeforeDate:兩個方法,前一個方法會嘗試加鎖,如果鎖不可用(已經(jīng)被鎖住),剛并不會阻塞線程,并返回NO。lockBeforeDate:方法會在所指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO。
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
2.4 NSRecursiveLock
NSRecursiveLock是一個遞歸鎖,這個鎖可以被同一線程多次請求,而不會引起死鎖。這主要是用在循環(huán)或遞歸操作中。
//NSLock *lock = [[NSLock alloc] init];
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
這段代碼是一個典型的死鎖情況。在我們的線程中,RecursiveMethod是遞歸調(diào)用的。所以每次進(jìn)入這個block時,都會去加一次鎖,而從第二次開始,由于鎖已經(jīng)被使用了且沒有解鎖,所以它需要等待鎖被解除,這樣就導(dǎo)致了死鎖,線程被阻塞住了。
在這種情況下,我們就可以使用NSRecursiveLock。它可以允許同一線程多次加鎖,而不會造成死鎖。遞歸鎖會跟蹤它被lock的次數(shù)。每次成功的lock都必須平衡調(diào)用unlock操作。只有所有達(dá)到這種平衡,鎖最后才能被釋放,以供其它線程使用。
2.5 NSConditionLock
當(dāng)我們在使用多線程的時候,有時一把只會lock和unlock的鎖未必就能完全滿足我們的使用。因為普通的鎖只能關(guān)心鎖與不鎖,而不在乎用什么鑰匙才能開鎖,而我們在處理資源共享的時候,多數(shù)情況是只有滿足一定條件的情況下才能打開這把鎖。
@interface NSConditionLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
lock -> 表示 xxx 期待獲得鎖,如果沒有其他線程獲得鎖(不需要判斷內(nèi)部的condition) 那它能執(zhí)行此行以下代碼,如果已經(jīng)有其他線程獲得鎖(可能是條件鎖,或者無條件鎖),則等待,直至其他線程解鎖。
lockWhenCondition:A條件 -> 表示如果沒有其他線程獲得該鎖,但是該鎖內(nèi)部的condition不等于A條件,它依然不能獲得鎖,仍然等待。如果內(nèi)部的condition等于A條件,并且沒有其他線程獲得該鎖,則進(jìn)入代碼區(qū),同時設(shè)置它獲得該鎖,其他任何線程都將等待它代碼的完成,直至它解鎖。
unlockWithCondition:A條件 -> 表示釋放鎖,同時把內(nèi)部的condition設(shè)置為A條件。
例子解析:
//主線程中
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lockWhenCondition:1];
NSLog(@"線程1");
sleep(2);
[lock unlock];
});
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保證讓線程2的代碼后執(zhí)行
if ([lock tryLockWhenCondition:0]) {
NSLog(@"線程2");
[lock unlockWithCondition:2];
NSLog(@"線程2解鎖成功");
} else {
NSLog(@"線程2嘗試加鎖失敗");
}
});
//線程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);//以保證讓線程2的代碼后執(zhí)行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"線程3");
[lock unlock];
NSLog(@"線程3解鎖成功");
} else {
NSLog(@"線程3嘗試加鎖失敗");
}
});
//線程4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);//以保證讓線程2的代碼后執(zhí)行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"線程4");
[lock unlockWithCondition:1];
NSLog(@"線程4解鎖成功");
} else {
NSLog(@"線程4嘗試加鎖失敗");
}
});
2016-08-19 13:51:15.353 ThreadLockControlDemo[1614:110697] 線程2
2016-08-19 13:51:15.354 ThreadLockControlDemo[1614:110697] 線程2解鎖成功
2016-08-19 13:51:16.353 ThreadLockControlDemo[1614:110689] 線程3
2016-08-19 13:51:16.353 ThreadLockControlDemo[1614:110689] 線程3解鎖成功
2016-08-19 13:51:17.354 ThreadLockControlDemo[1614:110884] 線程4
2016-08-19 13:51:17.355 ThreadLockControlDemo[1614:110884] 線程4解鎖成功
2016-08-19 13:51:17.355 ThreadLockControlDemo[1614:110884] 線程1
上面代碼先輸出了 ”線程 2“,因為線程 1 的加鎖條件不滿足,初始化時候的 condition 參數(shù)為 0,而加鎖條件是 condition 為 1,所以加鎖失敗。locakWhenCondition 與 lock 方法類似,加鎖失敗會阻塞線程,所以線程 1 會被阻塞著,而 tryLockWhenCondition 方法就算條件不滿足,也會返回 NO,不會阻塞當(dāng)前線程。
回到上面的代碼,線程 2 執(zhí)行了 [lock unlockWithCondition:2]; 所以 Condition 被修改成了 2。
而線程 3 的加鎖條件是 Condition 為 2, 所以線程 3 才能加鎖成功,線程 3 執(zhí)行了 [lock unlock]; 解鎖成功且不改變 Condition 值。
線程 4 的條件也是 2,所以也加鎖成功,解鎖時將 Condition 改成 1。這個時候線程 1 終于可以加鎖成功,解除了阻塞。
從上面可以得出,NSConditionLock 還可以實現(xiàn)任務(wù)之間的依賴。
2.6 NSCodition
NSCondition 的對象實際上作為一個鎖和一個線程檢查器:鎖主要為了當(dāng)檢測條件時保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù);線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程,即線程是否被阻塞。
以下內(nèi)容摘自官網(wǎng):
The semantics for using an NSCondition object are as follows:
Lock the condition object.
Test a boolean predicate. (This predicate is a boolean flag or other variable in your code that indicates whether it is safe to perform the task protected by the condition.)
If the boolean predicate is false, call the condition object’s wait() or wait(until:) method to block the thread. Upon returning from these methods, go to step 2 to retest your boolean predicate. (Continue waiting and retesting the predicate until it is true.)
If the boolean predicate is true, perform the task.
Optionally update any predicates (or signal any conditions) affected by your task.
When your task is done, unlock the condition object.
The pseudocode for performing the preceding steps would therefore look something like the following:
lock the condition
while (!(boolean_predicate)) {
wait on condition
}
do protected work
(optionally, signal or broadcast the condition again or change a predicate value)
unlock the condition
以下是翻譯(感謝 AidenRao):
鎖定條件對象。
測試是否可以安全的履行接下來的任務(wù)。
如果布爾值是假的,調(diào)用條件對象的 wait 或 waitUntilDate: 方法來阻塞線程。 在從這些方法返回,則轉(zhuǎn)到步驟 2 重新測試你的布爾值。 (繼續(xù)等待信號和重新測試,直到可以安全的履行接下來的任務(wù)。waitUntilDate: 方法有個等待時間限制,指定的時間到了,則放回 NO,繼續(xù)運(yùn)行接下來的任務(wù))
如果布爾值為真,執(zhí)行接下來的任務(wù)。
當(dāng)任務(wù)完成后,解鎖條件對象。
步驟 3 說的等待的信號,既線程 2 執(zhí)行 [lock signal] 發(fā)送的信號。
其中 signal 和 broadcast 方法的區(qū)別在于,signal 只是一個信號量,只能喚醒一個等待的線程,想喚醒多個就得多次調(diào)用,而 broadcast 可以喚醒所有在等待的線程。如果沒有等待的線程,這兩個方法都沒有作用。
典型的生產(chǎn)者和消費者案例簡析:
- 消費者取得鎖,取產(chǎn)品,如果沒有,則wait,這時會釋放鎖,直到有線程喚醒它去消費產(chǎn)品;
- 生產(chǎn)者制造產(chǎn)品,首先也要取得鎖,然后生產(chǎn),再發(fā)signal,這樣可喚醒wait的消費者。
2.7 pthread_mutex
int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);
int pthread_mutex_lock(pthread_mutex_t *);// 加鎖
int pthread_mutex_trylock(pthread_mutex_t *);// 加鎖,但是與2不一樣的是當(dāng)鎖已經(jīng)在使用的時候,返回為EBUSY,而不是掛起等
int pthread_mutex_unlock(pthread_mutex_t *);// 釋放鎖
int pthread_mutex_destroy(pthread_mutex_t *);
int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
int * __restrict);
int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
int * __restrict);
示例代碼:
static pthread_mutex_t theLock;
- (void)example5 {
pthread_mutex_init(&theLock, NULL);
pthread_t thread;
pthread_create(&thread, NULL, threadMethord1, NULL);
pthread_t thread2;
pthread_create(&thread2, NULL, threadMethord2, NULL);
}
void *threadMethord1() {
pthread_mutex_lock(&theLock);
printf("線程1\n");
sleep(2);
pthread_mutex_unlock(&theLock);
printf("線程1解鎖成功\n");
return 0;
}
void *threadMethord2() {
sleep(1);
pthread_mutex_lock(&theLock);
printf("線程2\n");
pthread_mutex_unlock(&theLock);
return 0;
}
線程1
線程1解鎖成功
線程2
const pthread_mutexattr_t * __restrict參數(shù)值類型:
PTHREAD_MUTEX_NORMAL 缺省類型,也就是普通鎖。當(dāng)一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列,并在解鎖后先進(jìn)先出原則獲得鎖。
PTHREAD_MUTEX_ERRORCHECK 檢錯鎖,如果同一個線程請求同一個鎖,則返回 EDEADLK,否則與普通鎖類型動作相同。這樣就保證當(dāng)不允許多次加鎖時不會出現(xiàn)嵌套情況下的死鎖。
PTHREAD_MUTEX_RECURSIVE 遞歸鎖,允許同一個線程對同一個鎖成功獲得多次,并通過多次 unlock 解鎖。
PTHREAD_MUTEX_DEFAULT 適應(yīng)鎖,動作最簡單的鎖類型,僅等待解鎖后重新競爭,沒有等待隊列。
2.8 pthread_mutex(recursive)
pthread_mutex為了防止在遞歸的情況下出現(xiàn)死鎖而出現(xiàn)的遞歸鎖。作用和NSRecursiveLock遞歸鎖類似。
__block pthread_mutex_t theLock;
//pthread_mutex_init(&theLock, NULL);
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
pthread_mutex_lock(&theLock);
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
pthread_mutex_unlock(&theLock);
};
RecursiveMethod(5);
});
如果使用pthread_mutex_init(&theLock, NULL);初始化鎖的話,上面的代碼會出現(xiàn)死鎖現(xiàn)象。如果使用遞歸鎖的形式,則沒有問題。
2.9 OSSpinLock
OSSpinLock 自旋鎖,性能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當(dāng)?shù)却龝r會消耗大量 CPU 資源,所以它不適用于較長時間的任務(wù)。 不過最近YY大神在自己的博客不再安全的OSSpinLock中說明了OSSpinLock已經(jīng)不再安全,請大家謹(jǐn)慎使用。
參考文章:(可以按我列出的文章順序挨個閱讀一遍)
- iOS中保證線程安全的幾種方式與性能對比 - 快速瀏覽
- iOS 常見知識點(三):Lock - 鞏固復(fù)習(xí)
- bestswifter-深入理解iOS開發(fā)中的鎖 - 深入理解
- 不再安全的OSSpinLock - 拓展閱讀
- 關(guān)于 @synchronized,這兒比你想知道的還要多 - 深度好文