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

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

lock_benchmark.png

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

于是懷著慚愧的心情趕緊把這些鎖學(xué)習(xí)了下,廢話不多說(shuō),我們開(kāi)始:

鎖 是什么意思?

我們?cè)谑褂枚嗑€程的時(shí)候多個(gè)線程可能會(huì)訪問(wèn)同一塊資源,這樣就很容易引發(fā)數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)安全等問(wèn)題,這時(shí)候就需要我們保證每次只有一個(gè)線程訪問(wèn)這一塊資源, 應(yīng)運(yùn)而生。

OSSpinLock


需導(dǎo)入頭文件:

#import <libkern/OSAtomic.h>

例子:

__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"線程1 準(zhǔn)備上鎖");
    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 準(zhǔn)備上鎖");
    OSSpinLockLock(&oslock);
    NSLog(@"線程2");
    OSSpinLockUnlock(&oslock);
    NSLog(@"線程2 解鎖成功");
});

運(yùn)行結(jié)果:

OSSpinLock1

我們來(lái)修改一下代碼:

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

運(yùn)行結(jié)果:

OSSpinLock2

OSSpinLock1 圖中可以發(fā)現(xiàn):當(dāng)我們鎖住線程1時(shí),在同時(shí)鎖住線程2的情況下,線程2會(huì)一直等待(自旋鎖不會(huì)讓等待的進(jìn)入睡眠狀態(tài)),直到線程1的任務(wù)執(zhí)行完且解鎖完畢,線程2會(huì)立即執(zhí)行;而在 OSSpinLock2 圖中,因?yàn)槲覀冏⑨尩袅司€程1中的解鎖代碼,會(huì)繞過(guò)線程1,直到調(diào)用了線程2的解鎖方法才會(huì)繼續(xù)執(zhí)行線程1中的任務(wù),正常情況下,lockunlock最好成對(duì)出現(xiàn)

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

這里順便提一下trylocklock使用場(chǎng)景:

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

dispatch_semaphore 信號(hào)量


例子:

dispatch_semaphore_t signal = dispatch_semaphore_create(1); //傳入值必須 >=0, 若傳入為0則阻塞線程并等待timeout,時(shí)間到后會(huì)執(zhí)行其后的語(yǔ)句
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ā)送信號(hào)");
    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ā)送信號(hào)");
});

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

關(guān)于信號(hào)量,我們可以用停車來(lái)比喻:

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

運(yùn)行結(jié)果:

初始信號(hào)量大于0

可以發(fā)現(xiàn),因?yàn)槲覀兂跏蓟盘?hào)量的時(shí)候是大于 0 的,所以并沒(méi)有阻塞線程,而是直接執(zhí)行了 線程1 線程2。

我們把 信號(hào)量初始值改為 0:

dispatch_semaphore_t signal = dispatch_semaphore_create(0);

運(yùn)行結(jié)果:

初始信號(hào)量為0

可以看到這時(shí)候我們?cè)O(shè)置的 overTime 生效了。

pthread_mutex


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

使用需導(dǎo)入頭文件:

#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 準(zhǔn)備上鎖");
    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 準(zhǔn)備上鎖");
    pthread_mutex_lock(&pLock);
    NSLog(@"線程2");
    pthread_mutex_unlock(&pLock);
});

運(yùn)行結(jié)果:

pthread_mutex

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

這里貼個(gè) YYKit 中的源碼:

YYKit

pthread_mutex(recursive)


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

例子:

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

//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);
});

運(yùn)行結(jié)果:

結(jié)果

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

NSLock


NSLock API 很少也很簡(jiǎn)單:

NSLock

lock、unlock:不多做解釋,和上面一樣
trylock:能加鎖返回 YES 并執(zhí)行加鎖操作,相當(dāng)于 lock,反之返回 NO
** lockBeforeDate:這個(gè)方法表示會(huì)在傳入的時(shí)間內(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(@"失敗");
    }
});

運(yùn)行結(jié)果:

NSLock_result

NSCondition


我們先來(lái)看看 API:

NSCondition

看字面意思很好理解:

wait:進(jìn)入等待狀態(tài)
waitUntilDate::讓一個(gè)線程等待一定的時(shí)間
signal:?jiǎn)拘岩粋€(gè)等待的線程
broadcast:?jiǎn)拘阉械却木€程

例子:

  • 等待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];
});

結(jié)果:

waiting 2秒
  • 喚醒一個(gè)等待線程
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(@"喚醒一個(gè)等待的線程");
    [cLock signal];
});

結(jié)果:

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

運(yùn)行結(jié)果:

喚醒所有的線程

NSRecursiveLock


上面已經(jīng)大概介紹過(guò)了:
遞歸鎖可以被同一線程多次請(qǐng)求,而不會(huì)引起死鎖。這主要是用在循環(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);
});

運(yùn)行結(jié)果:

錯(cuò)誤信息

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

將 NSLock 替換為 NSRecursiveLock:

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

運(yùn)行結(jié)果:

NSRecursiveLock

NSRecursiveLock 方法里還提供了兩個(gè)方法,用法和上面介紹的基本沒(méi)什么差別,這里不過(guò)多介紹了:

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

@synchronized


@synchronized 相信大家應(yīng)該都熟悉,它的用法應(yīng)該算這些鎖中最簡(jiǎn)單的:

//線程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");
    }
});

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

NSConditionLock 條件鎖


我們先來(lái)看看 API :

NSConditionLock

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

例子:

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];
});

運(yùn)行結(jié)果:

result
  • 我們?cè)诔跏蓟?NSConditionLock 對(duì)象時(shí),給了他的標(biāo)示為 0
  • 執(zhí)行 tryLockWhenCondition:時(shí),我們傳入的條件標(biāo)示也是 0,所 以線程1 加鎖成功
  • 執(zhí)行 unlockWithCondition:時(shí),這時(shí)候會(huì)把condition0 修改為 1
  • 因?yàn)?code>condition 修改為了 1, 會(huì)先走到 線程3,然后 線程3 又將 condition 修改為 3
  • 最后 走了 線程2 的流程

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

參考文獻(xiàn):

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評(píng)論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,923評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 175,041評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,475評(píng)論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,253評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 54,801評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,023評(píng)論 0 285
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,530評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,494評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,639評(píng)論 1 366
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,890評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,289評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,552評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,242評(píng)論 3 389
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,626評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容