iOS 開發(fā)中的八種鎖(Lock)

這兩天翻看 ibireme 大神 《不再安全的 OSSpinLock》 這篇文章,看到文中分析各種鎖之前的性能的圖表:

lock_benchmark.png

發(fā)現(xiàn)除了@synchronized 用過,其他的都陌生的很,可以說完全不知道....

于是懷著慚愧的心情趕緊把這些鎖學習了下,廢話不多說,我們開始:

鎖 是什么意思?

我們在使用多線程的時候多個線程可能會訪問同一塊資源,這樣就很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全等問題,這時候就需要我們保證每次只有一個線程訪問這一塊資源, 應運而生。

OSSpinLock


需導入頭文件:

#import <libkern/OSAtomic.h>

例子:

__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程1 準備上鎖");
    OSSpinLockLock(&oslock);
    sleep(4);
    NSLog(@"線程1");
    OSSpinLockUnlock(&oslock);
    NSLog(@"線程1 解鎖成功");
    NSLog(@"--------------------------------------------------------");
});

//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程2 準備上鎖");
    OSSpinLockLock(&oslock);
    NSLog(@"線程2");
    OSSpinLockUnlock(&oslock);
    NSLog(@"線程2 解鎖成功");
});

運行結果:

OSSpinLock1

我們來修改一下代碼:

__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//線程1        
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
......
//OSSpinLockUnlock(&oslock);
......

運行結果:

OSSpinLock2

OSSpinLock1 圖中可以發(fā)現(xiàn):當我們鎖住線程1時,在同時鎖住線程2的情況下,線程2會一直等待(自旋鎖不會讓等待的進入睡眠狀態(tài)),直到線程1的任務執(zhí)行完且解鎖完畢,線程2會立即執(zhí)行;而在 OSSpinLock2 圖中,因為我們注釋掉了線程1中的解鎖代碼,會繞過線程1,直到調(diào)用了線程2的解鎖方法才會繼續(xù)執(zhí)行線程1中的任務,正常情況下,lockunlock最好成對出現(xiàn)。

OS_SPINLOCK_INIT: 默認值為 0,在 locked 狀態(tài)時就會大于 0unlocked狀態(tài)下為 0
OSSpinLockLock(&oslock):上鎖,參數(shù)為 OSSpinLock 地址
OSSpinLockUnlock(&oslock):解鎖,參數(shù)為 OSSpinLock 地址
OSSpinLockTry(&oslock):嘗試加鎖,可以加鎖則立即加鎖并返回 YES,反之返回 NO

這里順便提一下trylocklock使用場景:

當前線程鎖失敗,也可以繼續(xù)其它任務,用 trylock 合適
當前線程只有鎖成功后,才會做一些有意義的工作,那就 lock,沒必要輪詢 trylock

dispatch_semaphore 信號量


例子:

dispatch_semaphore_t signal = dispatch_semaphore_create(1); //傳入值必須 >=0, 若傳入為0則阻塞線程并等待timeout,時間到后會執(zhí)行其后的語句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);

//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程1 等待ing");
    dispatch_semaphore_wait(signal, overTime); //signal 值 -1
    NSLog(@"線程1");
    dispatch_semaphore_signal(signal); //signal 值 +1
    NSLog(@"線程1 發(fā)送信號");
    NSLog(@"--------------------------------------------------------");
});

//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程2 等待ing");
    dispatch_semaphore_wait(signal, overTime);
    NSLog(@"線程2");
    dispatch_semaphore_signal(signal);
    NSLog(@"線程2 發(fā)送信號");
});

dispatch_semaphore_create(1): 傳入值必須 >=0, 若傳入為 0 則阻塞線程并等待timeout,時間到后會執(zhí)行其后的語句
dispatch_semaphore_wait(signal, overTime):可以理解為 lock,會使得 signal-1
dispatch_semaphore_signal(signal):可以理解為 unlock,會使得 signal+1

關于信號量,我們可以用停車來比喻:

停車場剩余4個車位,那么即使同時來了四輛車也能停的下。如果此時來了五輛車,那么就有一輛需要等待。
信號量的值(signal)就相當于剩余車位的數(shù)目,dispatch_semaphore_wait 函數(shù)就相當于來了一輛車,dispatch_semaphore_signal 就相當于走了一輛車。停車位的剩余數(shù)目在初始化的時候就已經(jīng)指明了(dispatch_semaphore_create(long value)),調(diào)用一次 dispatch_semaphore_signal,剩余的車位就增加一個;調(diào)用一次dispatch_semaphore_wait 剩余車位就減少一個;當剩余車位為 0 時,再來車(即調(diào)用 dispatch_semaphore_wait)就只能等待。有可能同時有幾輛車等待一個停車位。有些車主沒有耐心,給自己設定了一段等待時間,這段時間內(nèi)等不到停車位就走了,如果等到了就開進去停車。而有些車主就像把車停在這,所以就一直等下去。

運行結果:

初始信號量大于0

可以發(fā)現(xiàn),因為我們初始化信號量的時候是大于 0 的,所以并沒有阻塞線程,而是直接執(zhí)行了 線程1 線程2。

我們把 信號量初始值改為 0:

dispatch_semaphore_t signal = dispatch_semaphore_create(0);

運行結果:

初始信號量為0

可以看到這時候我們設置的 overTime 生效了。

pthread_mutex


ibireme 在《不再安全的 OSSpinLock》這篇文章中提到性能最好的 OSSpinLock 已經(jīng)不再是線程安全的并把自己開源項目中的 OSSpinLock 都替換成了 pthread_mutex。
特意去看了下源碼,總結了下常見用法:

使用需導入頭文件:

#import <pthread.h>

例子:

static pthread_mutex_t pLock;
pthread_mutex_init(&pLock, NULL);
 //1.線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程1 準備上鎖");
    pthread_mutex_lock(&pLock);
    sleep(3);
    NSLog(@"線程1");
    pthread_mutex_unlock(&pLock);
});

//1.線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程2 準備上鎖");
    pthread_mutex_lock(&pLock);
    NSLog(@"線程2");
    pthread_mutex_unlock(&pLock);
});

運行結果:

pthread_mutex

pthread_mutex 中也有個pthread_mutex_trylock(&pLock),和上面提到的 OSSpinLockTry(&oslock)區(qū)別在于,前者可以加鎖時返回的是 0,否則返回一個錯誤提示碼;后者返回的 YESNO

這里貼個 YYKit 中的源碼:

YYKit

pthread_mutex(recursive)


經(jīng)過上面幾種例子,我們可以發(fā)現(xiàn):加鎖后只能有一個線程訪問該對象,后面的線程需要排隊,并且 lock 和 unlock 是對應出現(xiàn)的,同一線程多次 lock 是不允許的,而遞歸鎖允許同一個線程在未釋放其擁有的鎖時反復對該鎖進行加鎖操作。

例子:

static pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且給它賦予默認
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //設置鎖類型,這邊是設置為遞歸鎖
pthread_mutex_init(&pLock, &attr);
pthread_mutexattr_destroy(&attr); //銷毀一個屬性對象,在重新進行初始化之前該結構不能重新使用

//1.線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        pthread_mutex_lock(&pLock);
        if (value > 0) {
            NSLog(@"value: %d", value);
            RecursiveBlock(value - 1);
        }
        pthread_mutex_unlock(&pLock);
    };
    RecursiveBlock(5);
});

運行結果:

結果

上面的代碼如果我們用 pthread_mutex_init(&pLock, NULL) 初始化會出現(xiàn)死鎖的情況,遞歸鎖能很好的避免這種情況的死鎖;

NSLock


NSLock API 很少也很簡單:

NSLock

lock、unlock:不多做解釋,和上面一樣
trylock:能加鎖返回 YES 并執(zhí)行加鎖操作,相當于 lock,反之返回 NO
** lockBeforeDate:這個方法表示會在傳入的時間內(nèi)嘗試加鎖,若能加鎖則執(zhí)行加鎖**操作并返回 YES,反之返回 NO

例子:

NSLock *lock = [NSLock new];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程1 嘗試加速ing...");
    [lock lock];
    sleep(3);//睡眠5秒
    NSLog(@"線程1");
    [lock unlock];
    NSLog(@"線程1解鎖成功");
});

//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程2 嘗試加速ing...");
    BOOL x =  [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
    if (x) {
        NSLog(@"線程2");
        [lock unlock];
    }else{
        NSLog(@"失敗");
    }
});

運行結果:

NSLock_result

NSCondition


我們先來看看 API:

NSCondition

看字面意思很好理解:

wait:進入等待狀態(tài)
waitUntilDate::讓一個線程等待一定的時間
signal:喚醒一個等待的線程
broadcast:喚醒所有等待的線程

例子:

  • 等待2秒
NSCondition *cLock = [NSCondition new];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"start");
    [cLock lock];
    [cLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
    NSLog(@"線程1");
    [cLock unlock];
});

結果:

waiting 2秒
  • 喚醒一個等待線程
NSCondition *cLock = [NSCondition new];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lock];
    NSLog(@"線程1加鎖成功");
    [cLock wait];
    NSLog(@"線程1");
    [cLock unlock];
});

//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lock];
    NSLog(@"線程2加鎖成功");
    [cLock wait];
    NSLog(@"線程2");
    [cLock unlock];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(2);
    NSLog(@"喚醒一個等待的線程");
    [cLock signal];
});

結果:

喚醒一個等待的線程
  • 喚醒所有等待的線程
.........    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(2);
    NSLog(@"喚醒所有等待的線程");
    [cLock broadcast];
});

運行結果:

喚醒所有的線程

NSRecursiveLock


上面已經(jīng)大概介紹過了:
遞歸鎖可以被同一線程多次請求,而不會引起死鎖。這主要是用在循環(huán)或遞歸操作中。

例子:

NSLock *rLock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        [rLock lock];
        if (value > 0) {
            NSLog(@"線程%d", value);
            RecursiveBlock(value - 1);
        }
        [rLock unlock];
    };
    RecursiveBlock(4);
});

運行結果:

錯誤信息

這段代碼是一個典型的死鎖情況。在我們的線程中,RecursiveMethod 是遞歸調(diào)用的。所以每次進入這個 block 時,都會去加一次鎖,而從第二次開始,由于鎖已經(jīng)被使用了且沒有解鎖,所以它需要等待鎖被解除,這樣就導致了死鎖,線程被阻塞住了。

將 NSLock 替換為 NSRecursiveLock:

NSRecursiveLock *rLock = [NSRecursiveLock new];
..........

運行結果:

NSRecursiveLock

NSRecursiveLock 方法里還提供了兩個方法,用法和上面介紹的基本沒什么差別,這里不過多介紹了:

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@synchronized


@synchronized 相信大家應該都熟悉,它的用法應該算這些鎖中最簡單的:

//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized (self) {
        sleep(2);
        NSLog(@"線程1");
    }
});

//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized (self) {
        NSLog(@"線程2");
    }
});

有興趣可以看一下這篇文章 《 關于 @synchronized,這兒比你想知道的還要多

NSConditionLock 條件鎖


我們先來看看 API :

NSConditionLock

相比于 NSLock 多了個 condition 參數(shù),我們可以理解為一個條件標示。

例子:

NSConditionLock *cLock = [[NSConditionLock alloc] initWithCondition:0];

//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    if([cLock tryLockWhenCondition:0]){
        NSLog(@"線程1");
       [cLock unlockWithCondition:1];
    }else{
         NSLog(@"失敗");
    }
});

//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lockWhenCondition:3];
    NSLog(@"線程2");
    [cLock unlockWithCondition:2];
});

//線程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lockWhenCondition:1];
    NSLog(@"線程3");
    [cLock unlockWithCondition:3];
});

運行結果:

result
  • 我們在初始化 NSConditionLock 對象時,給了他的標示為 0
  • 執(zhí)行 tryLockWhenCondition:時,我們傳入的條件標示也是 0,所 以線程1 加鎖成功
  • 執(zhí)行 unlockWithCondition:時,這時候會把condition0 修改為 1
  • 因為condition 修改為了 1, 會先走到 線程3,然后 線程3 又將 condition 修改為 3
  • 最后 走了 線程2 的流程

從上面的結果我們可以發(fā)現(xiàn),NSConditionLock 還可以實現(xiàn)任務之間的依賴。

參考文獻:

NSRecursiveLock遞歸鎖的使用
關于dispatch_semaphore的使用
實現(xiàn)鎖的多種方式和鎖的高級用法

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

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