【iOS】 常見的各種鎖Lock ??

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):

  1. 鎖定條件對象。

  2. 測試是否可以安全的履行接下來的任務(wù)。

  3. 如果布爾值是假的,調(diào)用條件對象的 wait 或 waitUntilDate: 方法來阻塞線程。 在從這些方法返回,則轉(zhuǎn)到步驟 2 重新測試你的布爾值。 (繼續(xù)等待信號和重新測試,直到可以安全的履行接下來的任務(wù)。waitUntilDate: 方法有個等待時間限制,指定的時間到了,則放回 NO,繼續(xù)運(yùn)行接下來的任務(wù))

  4. 如果布爾值為真,執(zhí)行接下來的任務(wù)。

  5. 當(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)慎使用。

參考文章:(可以按我列出的文章順序挨個閱讀一遍)

  1. iOS中保證線程安全的幾種方式與性能對比 - 快速瀏覽
  2. iOS 常見知識點(三):Lock - 鞏固復(fù)習(xí)
  3. bestswifter-深入理解iOS開發(fā)中的鎖 - 深入理解
  4. 不再安全的OSSpinLock - 拓展閱讀
  5. 關(guān)于 @synchronized,這兒比你想知道的還要多 - 深度好文
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容