iOS-關於鎖的總結

前言

對於iOS中各類鎖的學習總結,供往後查閱,如有任何問題,請各位大佬幫忙指正html

引子

平常開發中,@property (nonatomic, strong) *foo是咱們不厭其煩的使用頻率最高的聲明方式,也很清楚atomicnonatomic屬性的區別,這裏再複習一下這兩個關鍵字:ios

  • atomic:原子性,這個屬性是默認的,經過在settergetter中加鎖保證數據的讀寫安全
  • nonatomic:非原子性,就是不加鎖。優勢是速度優於使用atomic,大多數場景不會出現問題

做爲編譯器標識符,@property的做用是幫助咱們快速生成成員變量及其getter/setter方法,並經過屬性關鍵字,幫助咱們管理內存及安全等繁雜的事務,那麼atomic是如何幫助咱們保證成員變量的讀寫安全呢?下面咱們看一段代碼:objective-c

//@property(retain) UITextField *userName;
// 示例代碼以下:
- (UITextField *) userName {
    UITextField *retval = nil;
    @synchronized(self) {
        _userName = retval;
    }
    return retval;
}
- (void) setUserName:(UITextField *)userName {
    @synchronized(self) {
      [_userName release];
      _userName = [userName retain];
    }
}
複製代碼

咱們能夠很容易的看出,編譯器是經過加鎖,來保證當前成員變量_userName的讀寫安全,不至於生成髒數據,這即是atomic背後,編譯器幫咱們作的事情。事實上,若是深究下去編譯器幫咱們加了什麼鎖,其實並不是@synchronized(object)編程

自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active;不會使線程進入阻塞狀態(休眠),減小了沒必要要的上下文切換,執行速度快緩存

非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候須要從內核態恢復,須要線程上下文切換,影響鎖的性能安全

爲何atomic會作爲默認屬性,咱們不難看出,蘋果這麼設計是想告訴咱們,不少狀況下,效率換安全是值得的bash

如何使用鎖

下面一段簡單代碼,考慮一下輸出結果網絡

- (void)unlockTest {
    NSMutableString *string = [@"Mike" mutableCopy];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [string appendString:@"-Locked"];
        NSLog(@"%@",string);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [string appendString:@"-JailBreaked"];
        NSLog(@"%@",string);
    });
}  
複製代碼

書寫這樣一段代碼,是想在不一樣線程中在改變變量後,使用這個變量session

控制檯輸出:多線程

2019-11-11 16:52:43.019128+0800 DiscoverLock_iOS[89763:11225442] Mike-Locked-JailBreaked
2019-11-11 16:52:43.019128+0800 DiscoverLock_iOS[89763:11225441] Mike-Locked-JailBreaked
複製代碼

這顯然不是想要的結果,如何保證咱們在不一樣線程中使用的變量,都是咱們但願的值呢?答案之一,就是加鎖

NSLocking

OC爲咱們提供了四種遵循的類,分別是NSLock/NSCondtionLock/NSRecursiveLock/NSCondition,知足面向對象編程的需求

@protocol NSLocking

- (void)lock;// 阻塞線程,線程休眠
- (void)unlock;

@end
複製代碼

加鎖的基本流程: 【加鎖】->【操做】->【解鎖】 以上提到的4個類,都可以實現這個基礎功能,下文中再也不贅述

- (void)lockedTest {
    NSMutableString *string = [@"Mike" mutableCopy];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        // [鎖 lock];
        [string appendString:@"-Locked"];
        NSLog(@"%@",string);
        // [鎖 unlock];

    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // [鎖 lock];
        [string appendString:@"-JailBreaked"];
        NSLog(@"%@",string);
        // [鎖 unlock];
    });
}
複製代碼

控制檯輸出:

DiscoverLock_iOS[90562:11303793] Mike-Locked
DiscoverLock_iOS[90562:11303799] Mike-Locked-JailBreaked
複製代碼
DiscoverLock_iOS[90562:11303793] Mike-JailBreaked
DiscoverLock_iOS[90562:11303799] Mike-JailBreaked-Locked
複製代碼

這裏的輸出,結果不太同樣,側面說明了DISPATCH_QUEUE_PRIORITY並不能保證線程的執行順序,若是要明確執行順序,屬於線程同步的範疇,本文不展開討論,只會在NSConditionLock部分簡單示例如何使用該類作到同步

NSLock

  • - (BOOL)tryLock;:嘗試加鎖,若是失敗返回NO,不會阻塞線程
  • - (BOOL)lockBeforeDate:(NSDate *)limit;:指定時間前嘗試加鎖,若是失敗返回NO,到時間前阻塞線程

示例代碼:

- (void)lockTest {

    NSMutableString *string = [@"Mike" mutableCopy];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    LOCK(
         [string appendString:@"-Locked"];
         NSLog(@"%@",string);
         sleep(5);
        )
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    TRYLOCK(
            [string appendString:@"-UnLock"];
            NSLog(@"%@",string);
            sleep(3);
        )
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    TRYLOCKINDURATION(2,
                      [string appendString:@"-Ending"];
                      NSLog(@"%@",string);
                      );
    NSLog(@"-=-=-=-=-");
    });
}
複製代碼

控制檯輸出:

2019-11-11 19:54:08.807763+0800 DiscoverLock_iOS[92986:11465678] Mike-Locked
2019-11-11 19:54:08.807763+0800 DiscoverLock_iOS[92986:11465679] TryLock-NO
2019-11-11 19:54:08.807889+0800 DiscoverLock_iOS[92986:11465679] Mike-Locked-UnLock
2019-11-11 19:54:10.810165+0800 DiscoverLock_iOS[92986:11465677] TryLockBefore-NO
2019-11-11 19:54:10.810523+0800 DiscoverLock_iOS[92986:11465677] Mike-Locked-UnLock-Ending
2019-11-11 19:54:10.810810+0800 DiscoverLock_iOS[92986:11465677] -=-=-=-=-
複製代碼

經過上面示例代碼輸出能夠看到,- (BOOL)tryLock;並不會阻塞線程,在嘗試加鎖失敗時,當即返回了NO,可是- (BOOL)lockBeforeDate:(NSDate *)limit;則在時間到以前阻塞了線程操做,在等待相應時間後,返回了NO,並執行了下一句打印,很明顯是在等待期間阻塞了線程

上面代碼中用到的幾個宏定義,建議之後使用鎖時,儘可能保持頭腦清醒或者乾脆定義一些便利方法,保證【上鎖】-【解鎖】的成對出現,避免線程阻塞或死鎖的狀況

#define LOCK(...) \
[_lock lock]; \
__VA_ARGS__; \
[_lock unlock]; \

#define TRYLOCK(...) \
BOOL locked = [_lock tryLock]; \
NSLog(@"%@",locked?@"TryLock-YES":@"TryLock-NO"); \
__VA_ARGS__; \
if (locked) [_lock unlock]; \

#define TRYLOCKINDURATION(duration,...) \
BOOL locked = [_lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:duration]]; \
NSLog(@"%@",locked?@"TryLockBefore-YES":@"TryLockBefore-NO"); \
__VA_ARGS__; \
if (locked) [_lock unlock]; \
複製代碼

NSConditionLock

  • - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;:便利構造方法,傳入條件鎖的初始值
  • @property (readonly) NSInteger condition;:當前條件鎖的值
  • - (void)lockWhenCondition:(NSInteger)condition;:當鎖的條件值與傳入值相等時,執行接下來的操做,不然阻塞線程
  • - (BOOL)tryLock;:嘗試加鎖,若是失敗返回NO,不會阻塞線程
  • - (BOOL)tryLockWhenCondition:(NSInteger)condition;:嘗試加鎖,當鎖的條件值與傳入值相等,則加鎖成功,不然失敗返回NO,不會阻塞線程
  • - (void)unlockWithCondition:(NSInteger)condition;:解鎖操做,同時變動鎖的條件值爲傳入值
  • - (BOOL)lockBeforeDate:(NSDate *)limit;:指定時間前嘗試加鎖,若是失敗返回NO,到時間前阻塞線程
  • - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;:指定時間前嘗試加鎖,當鎖的條件值與傳入值相等,則加鎖成功返回YES,不然失敗返回NO,到時間前阻塞線程

NSConditionLockNSLock方法相似,多了一個condition屬性,以及每一個操做都多了一個關於condition屬性的方法,- (void)lockWhenCondition:(NSInteger)condition;只有condition參數與初始化時候的condition相等,lock才能正確進行加鎖操做。而- (void)unlockWithCondition:(NSInteger)condition;並非當條件值符合條件時才解鎖,而是解鎖以後,修改當前鎖的條件值 假如不使用condition相關的方法,NSConditionLockNSLock並沒有二致

上文中咱們提到了線程同步問題,這裏一塊兒看一下下面這段代碼

- (void)conditionLockUnordered {
    NSMutableString *conditionString = [[NSMutableString alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"-1-"];
        NSLog(@">>> 1 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"-2-"];
        NSLog(@">>> 2 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"-3-"];
        NSLog(@">>> 3 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"-4-"];
        NSLog(@">>> 4 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"-5-"];
        NSLog(@">>> 5 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
}
複製代碼

控制檯輸出:

2019-11-11 20:34:16.875479+0800 DiscoverLock_iOS[93895:11551560] >>> 2 -1--2--4--3- threadInfo:<NSThread: 0x600003905640>{number = 4, name = (null)}<<<
2019-11-11 20:34:16.875525+0800 DiscoverLock_iOS[93895:11551562] >>> 3 -1--2--4--3- threadInfo:<NSThread: 0x600003903680>{number = 6, name = (null)}<<<
2019-11-11 20:34:16.875530+0800 DiscoverLock_iOS[93895:11551561] >>> 1 -1--2- threadInfo:<NSThread: 0x600003908bc0>{number = 3, name = (null)}<<<
2019-11-11 20:34:16.875543+0800 DiscoverLock_iOS[93895:11551559] >>> 4 -1--2--4--3- threadInfo:<NSThread: 0x6000039175c0>{number = 5, name = (null)}<<<
2019-11-11 20:34:16.875628+0800 DiscoverLock_iOS[93895:11551560] >>> 5 -1--2--4--3--5- threadInfo:<NSThread: 0x600003905640>{number = 4, name = (null)}<<<
複製代碼

依然是混亂狀態,上文中NSLock部分已經經過加鎖,控制了讀寫的穩定性,那麼若是咱們想要按照標號依次執行,該如何操做?

熟悉GCD的小夥伴會說這還不簡單,dispatch_barrier解千愁,固然這麼寫沒問題,可是這裏多說一嘴,dispatch_barrier只能針對同一個併發隊列起做用,注意正確初始化的姿式dispatch_queue_t thread = dispatch_queue_create("barrier", DISPATCH_QUEUE_CONCURRENT);,而不是幹啥都是一句dispatch_get_global_queue(0,0),若是使用Global_Queue,這個barrier就同普通的dispatch_async沒什麼區別了

咱們要是想在不一樣線程搞定順序這個事兒,怎麼辦呢?這個時候NSConditionLock自帶的條件方法,便能幫你實現這個功能,具體看下面的示例代碼

- (void)conditionLockOrdered {
    // NSConditionLock
    NSInteger conditionTag = 0;
    _conditionLock = [[NSConditionLock alloc] initWithCondition:conditionTag];
    
    NSMutableString *conditionString = [[NSMutableString alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@">>> handle 1 <<<");
        [_conditionLock lockWhenCondition:conditionTag];
        [conditionString appendString:@"-1-"];
        NSLog(@">>> 1 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock unlockWithCondition:1];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@">>> handle 2 <<<");
        [_conditionLock lockWhenCondition:1];
        [conditionString appendString:@"-2-"];
        NSLog(@">>> 2 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock unlockWithCondition:2];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@">>> handle 3 <<<");
        [_conditionLock lockWhenCondition:2];
        [conditionString appendString:@"-3-"];
        NSLog(@">>> 3 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock unlockWithCondition:3];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@">>> handle 4 <<<");
        [_conditionLock lockWhenCondition:3];
        [conditionString appendString:@"-4-"];
        NSLog(@">>> 4 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock unlockWithCondition:4];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@">>> handle 5 <<<");
        [_conditionLock lockWhenCondition:4];
        [conditionString appendString:@"-5-"];
        NSLog(@">>> 5 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock  unlock];
        NSLog(@"-=-=-=-=-=-=-");
    });
    NSLog(@"🍺");
}
複製代碼

控制檯輸出:

2019-11-11 20:53:58.237847+0800 DiscoverLock_iOS[94374:11586439] 🍺
2019-11-11 20:53:58.237862+0800 DiscoverLock_iOS[94374:11586488] >>> handle 1 <<<
2019-11-11 20:53:58.237877+0800 DiscoverLock_iOS[94374:11586489] >>> handle 3 <<<
2019-11-11 20:53:58.237868+0800 DiscoverLock_iOS[94374:11586490] >>> handle 2 <<<
2019-11-11 20:53:58.237887+0800 DiscoverLock_iOS[94374:11586491] >>> handle 4 <<<
2019-11-11 20:53:58.237892+0800 DiscoverLock_iOS[94374:11586495] >>> handle 5 <<<
2019-11-11 20:53:58.238111+0800 DiscoverLock_iOS[94374:11586488] >>> 1 -1- threadInfo:<NSThread: 0x6000014c3380>{number = 3, name = (null)}<<<
2019-11-11 20:53:58.238488+0800 DiscoverLock_iOS[94374:11586490] >>> 2 -1--2- threadInfo:<NSThread: 0x6000014dac40>{number = 4, name = (null)}<<<
2019-11-11 20:53:58.238605+0800 DiscoverLock_iOS[94374:11586489] >>> 3 -1--2--3- threadInfo:<NSThread: 0x6000014daf00>{number = 5, name = (null)}<<<
2019-11-11 20:53:58.239269+0800 DiscoverLock_iOS[94374:11586491] >>> 4 -1--2--3--4- threadInfo:<NSThread: 0x6000014c6740>{number = 6, name = (null)}<<<
2019-11-11 20:53:58.239410+0800 DiscoverLock_iOS[94374:11586495] >>> 5 -1--2--3--4--5- threadInfo:<NSThread: 0x6000014c3480>{number = 7, name = (null)}<<<
2019-11-11 20:53:58.239552+0800 DiscoverLock_iOS[94374:11586495] -=-=-=-=-=-=-
複製代碼

能夠看到,不一樣的線程,雖然被調度的時機不一樣,可是由於NSConditionLock的存在,後續對數據具體的操做,咱們預想的順序獲得了保證。這種用法筆者並認爲在任務耗時較少的狀況下沒有明顯問題的,可是假如存在長時間的耗時操做,仍是建議使用dispatch_barrier,由於這樣不會佔用過多資源

NSRecursiveLock

  • - (BOOL)tryLock;:嘗試加鎖,若是失敗返回NO,不會阻塞線程
  • - (BOOL)lockBeforeDate:(NSDate *)limit;:指定時間前嘗試加鎖,若是失敗返回NO,到時間前阻塞線程 Api同NSLock徹底同樣,區別在於NSRecursiveLock(遞歸鎖)能夠在同一線程中重複加鎖而不死鎖,它會記錄【上鎖】和【解鎖】的次數,當這兩個值平衡時,纔會釋放鎖,其餘線程才能夠上鎖成功

先看下一段代碼,會存在什麼問題:

@property (nonatomic, assign) NSInteger recursiveNum;// 5
- (void)test_unrecursiveLock {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self recursiveTest];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        self.recursiveNum = 7;
        NSLog(@">>> changed %ld <<<",self.recursiveNum);
    });
}

- (void)recursiveTest {
    if (self.recursiveNum > 0) {
        self.recursiveNum -= 1;
        NSLog(@">>> %ld <<<",self.recursiveNum);
        [self recursiveTest];
    }
}
複製代碼

控制檯輸出:

2019-11-11 21:27:13.451703+0800 DiscoverLock_iOS[95105:11645279] >>> 4 <<<
2019-11-11 21:27:13.451709+0800 DiscoverLock_iOS[95105:11645277] >>> changed 7 <<<
2019-11-11 21:27:13.451812+0800 DiscoverLock_iOS[95105:11645279] >>> 6 <<<
2019-11-11 21:27:13.451883+0800 DiscoverLock_iOS[95105:11645279] >>> 5 <<<
2019-11-11 21:27:13.451940+0800 DiscoverLock_iOS[95105:11645279] >>> 4 <<<
2019-11-11 21:27:13.452004+0800 DiscoverLock_iOS[95105:11645279] >>> 3 <<<
2019-11-11 21:27:13.452068+0800 DiscoverLock_iOS[95105:11645279] >>> 2 <<<
2019-11-11 21:27:13.452130+0800 DiscoverLock_iOS[95105:11645279] >>> 1 <<<
2019-11-11 21:27:13.452241+0800 DiscoverLock_iOS[95105:11645279] >>> 0 <<<
複製代碼

同時存在兩個線程,對已知的recursiveNum的值進行寫操做,其中一個線程使用遞歸調用,對該值進行了操做,可是同時另外一個線程改變了這個值,在不加鎖的狀況下,這種操做問題不少,若是遞歸中含有重要的邏輯處理,競態可能致使整個邏輯執行完的結果大機率是錯誤的。

如何規避這種競態致使的沒必要要的錯誤,首先咱們想到的是加鎖,可是若是遞歸加鎖的話,線程會重複加鎖,致使死鎖。因此這時候必須使用遞歸鎖來解決這個問題

- (void)test_unrecursiveLock {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self recursiveTest];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [_recursiveLock lock];// 遞歸鎖
        self.recursiveNum = 7;
        NSLog(@">>> changed %ld <<<",self.recursiveNum);
        [_recursiveLock unlock];// 解鎖
    });
}
- (void)recursiveTest {
    [_recursiveLock lock];// 遞歸鎖
    if (self.recursiveNum > 0) {
        self.recursiveNum -= 1;
        NSLog(@">>> %ld <<<",self.recursiveNum);
        [self recursiveTest];
    }
    [_recursiveLock unlock];// 解鎖
}
複製代碼

控制檯輸出:

2019-11-11 21:34:44.422337+0800 DiscoverLock_iOS[95341:11655990] >>> 4 <<<
2019-11-11 21:34:44.422442+0800 DiscoverLock_iOS[95341:11655990] >>> 3 <<<
2019-11-11 21:34:44.422511+0800 DiscoverLock_iOS[95341:11655990] >>> 2 <<<
2019-11-11 21:34:44.422583+0800 DiscoverLock_iOS[95341:11655990] >>> 1 <<<
2019-11-11 21:34:44.422645+0800 DiscoverLock_iOS[95341:11655990] >>> 0 <<<
2019-11-11 21:34:44.422747+0800 DiscoverLock_iOS[95341:11655992] >>> changed 7 <<<

------

2019-11-11 21:37:11.238448+0800 DiscoverLock_iOS[95396:11662426] >>> changed 7 <<<
2019-11-11 21:37:11.238635+0800 DiscoverLock_iOS[95396:11662423] >>> 6 <<<
2019-11-11 21:37:11.238793+0800 DiscoverLock_iOS[95396:11662423] >>> 5 <<<
2019-11-11 21:37:11.238930+0800 DiscoverLock_iOS[95396:11662423] >>> 4 <<<
2019-11-11 21:37:11.239093+0800 DiscoverLock_iOS[95396:11662423] >>> 3 <<<
2019-11-11 21:37:11.239293+0800 DiscoverLock_iOS[95396:11662423] >>> 2 <<<
2019-11-11 21:37:11.239844+0800 DiscoverLock_iOS[95396:11662423] >>> 1 <<<
2019-11-11 21:37:11.239976+0800 DiscoverLock_iOS[95396:11662423] >>> 0 <<<
複製代碼

雖然存在兩種輸出結果,可是咱們的遞歸操做的邏輯,能夠徹底不受干擾,若是須要控制順序,(敲黑板)要怎麼作呢?

NSCondition

  • - (void)wait;:當前線程當即進入休眠狀態
  • - (BOOL)waitUntilDate:(NSDate *)limit;:當前線程當即進入休眠狀態,limit時間後喚醒
  • - (void)signal;:喚醒wait後進入休眠的單條線程
  • - (void)broadcast;:喚醒wait後進入休眠的全部線程,調度

有些狀況須要協調線程之間的執行。例如,一個線程可能須要等待其餘線程返回結果,這個時候NSCondition多是個好選擇

爲了能體現NSCondition的做用,咱們舉一個可能並非很恰當的生產者-消費者的例子: 咱們如今有一條柔性生產線,限定每一個批次只能生產3件商品,耗時6s,同時開放網絡購買平臺讓你們搶購拼團,訂單式銷售,三人成團,如今有三位天選之子Tom/Mike/Lily從全球千萬人中脫穎而出,成功成團。爲了加強可玩性,活動是從開啓的一刻起,同時開始生產和搶購,3件庫存銷售完成後,再次進行同時進行生產和搶購活動

代碼示例以下:

@interface Producer : NSObject
@property (nonatomic, assign) BOOL shouldProduce;
@property (nonatomic, strong) NSString *itemName;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, strong) NSMutableArray *collector;

- (instancetype)initWithConditon:(NSCondition *)condition collector:(NSMutableArray *)collector;
- (void)produce;
@end

@implementation Producer

- (instancetype)initWithConditon:(NSCondition *)condition collector:(NSMutableArray *)collector{
    
    self = [super init];
    if (self) {
        self.condition = condition;
        self.collector = collector;
        self.shouldProduce = NO;
        self.itemName = nil;
    }
    return self;
}

-(void)produce{
    self.shouldProduce = YES;
    while (self.shouldProduce) {
        NSLog(@"準備生產");
        [self.condition lock];
        NSLog(@"- p lock -");
        if (self.collector.count > 0 ) {
            NSLog(@"- p - wait");
            [self.condition wait];
        }
        NSLog(@"開始生產");
        [self.collector addObject:@"商品1"];
        [self.collector addObject:@"商品2"];
        [self.collector addObject:@"商品3"];
        NSLog(@"生產:商品1/商品2/商品3");
        sleep(6);
        NSLog(@"生產結束");
        [self.condition broadcast];
        NSLog(@"- p signal -");
        [self.condition unlock];
        NSLog(@"- p unlock -");
    }
    NSLog(@"-結束生產-");
}

@end

@interface Consumer : NSObject
@property (nonatomic, assign) BOOL shouldConsumer;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, strong) NSMutableArray *collector;
@property (nonatomic,   copy) NSString *itemName;
- (instancetype)initWithConditon:(NSCondition *)condition collector:(NSMutableArray *)collector name:(NSString *)name;
- (void)consumer;
@end

@implementation Consumer
- (instancetype)initWithConditon:(NSCondition *)condition collector:(NSMutableArray *)collector name:(NSString *)name{
    self = [super init];
    if (self) {
        self.condition = condition;
        self.collector = collector;
        self.shouldConsumer = NO;
        self.itemName = name;
    }
    return self;
}

-(void)consumer{
    self.shouldConsumer = YES;
    while (self.shouldConsumer) {
        NSLog(@"%@-準備購買",self.itemName);
        [self.condition lock];
        NSLog(@"- c:%@ lock -",self.itemName);
        if (self.collector.count == 0 ) {
            NSLog(@"- c:%@ wait -",self.itemName);
            [self.condition wait];
        }
        NSString *item = [self.collector objectAtIndex:0];
        NSLog(@"%@-買入:%@",self.itemName,item);
        [self.collector removeObjectAtIndex:0];
        sleep(2);
        [self.condition signal];
        NSLog(@"- c:%@ signal -",self.itemName);
        [self.condition unlock];
        NSLog(@"- c:%@ unlock -",self.itemName);
    }
    NSLog(@"-%@結束購買-",self.itemName);
}
@end

// 調用
{
    NSMutableArray *pipeline = [NSMutableArray array];
    NSCondition *condition = [NSCondition new];
    
    Producer *p = [[Producer alloc] initWithConditon:condition collector:pipeline];
    Consumer *c = [[Consumer alloc] initWithConditon:condition collector:pipeline name:@"Tom"];
    Consumer *c1 = [[Consumer alloc] initWithConditon:condition collector:pipeline name:@"Mike"];
    Consumer *c2 = [[Consumer alloc] initWithConditon:condition collector:pipeline name:@"Lily"];
    [[[NSThread alloc] initWithTarget:c selector:@selector(consumer) object:c] start];
    [[[NSThread alloc] initWithTarget:c1 selector:@selector(consumer) object:c] start];
    [[[NSThread alloc] initWithTarget:c2 selector:@selector(consumer) object:c] start];
    [[[NSThread alloc] initWithTarget:p selector:@selector(produce) object:p] start];
    

    sleep(15);
    NSLog(@"<----------------->");
    p.shouldProduce = NO;
    c.shouldConsumer = NO;
    c1.shouldConsumer = NO;
    c2.shouldConsumer = NO;
}
複製代碼

部分控制檯輸出:

2019-11-12 17:04:03.662926+0800 DiscoverLock_iOS[7110:12246052] Mike-準備購買
2019-11-12 17:04:03.662916+0800 DiscoverLock_iOS[7110:12246051] Tom-準備購買
2019-11-12 17:04:03.662990+0800 DiscoverLock_iOS[7110:12246053] Lily-準備購買
2019-11-12 17:04:03.663005+0800 DiscoverLock_iOS[7110:12246054] 準備生產
2019-11-12 17:04:03.663083+0800 DiscoverLock_iOS[7110:12246053] - c:Lily lock -
2019-11-12 17:04:03.663144+0800 DiscoverLock_iOS[7110:12246053] - c:Lily wait -
2019-11-12 17:04:03.663254+0800 DiscoverLock_iOS[7110:12246052] - c:Mike lock -
2019-11-12 17:04:03.663439+0800 DiscoverLock_iOS[7110:12246052] - c:Mike wait -
2019-11-12 17:04:03.663805+0800 DiscoverLock_iOS[7110:12246051] - c:Tom lock -
2019-11-12 17:04:03.663903+0800 DiscoverLock_iOS[7110:12246051] - c:Tom wait -
2019-11-12 17:04:03.664126+0800 DiscoverLock_iOS[7110:12246054] - p lock -
2019-11-12 17:04:03.664297+0800 DiscoverLock_iOS[7110:12246054] 開始生產
2019-11-12 17:04:03.664433+0800 DiscoverLock_iOS[7110:12246054] 生產:商品1/商品2/商品3
2019-11-12 17:04:09.669735+0800 DiscoverLock_iOS[7110:12246054] 生產結束
複製代碼

基於多線程併發的工做原理,經過上面的部分打印結果,也很容易獲得這個結論。因爲不符合購買條件Lily/Mike/Tom都只能選擇wait,這個時候,生產者獲取到鎖並執行生產代碼,在生產完成後,broadcast或者signal告訴其餘線程,能夠喚醒線程並繼續執行消費者相關代碼。 NSCondition相較於NSConditionLock的不一樣點在於他依賴的是外部值,可以知足更多複雜需求場景。 假如將上述代碼中生產者的broadcast替換成signal後發現,在當前這種特定場景下,這兩個方法的做用彷佛並無什麼區別。並且感興趣的同窗,可使用上述代碼多運行幾回,看看是否可以得出同筆者同樣的猜想:

  1. NSCondition會自身經過隊列管理協同任務的調度
  2. wait的任務依次入等待隊列
  3. 未wait的任務根據得到鎖的順序依次入執行隊列
  4. wait任務的等待隊列會在執行隊列執行完後依次執行併入執行隊列
  5. 第一次調度順序肯定後,後續任務的執行,按照執行隊列緩存依次出列執行 這裏僅作猜測,具體實現可能並不是如此,待大佬指點迷津或有機會鶸筆者自行研究

OSSpinLock

看了NSCondition這麼個複雜的東西,咱們看點輕鬆的,OSSpinLock是蘋果在iOS10以前提供的自旋鎖方案,可是存在優先級反轉的問題,被蘋果廢棄,之前源碼中使用OSSpinLock的地方,都被蘋果替換成了pthread_mutex

os_unfair_lock

os_unfair_lockiOS10之後新增的低級別加鎖方案,本質是互斥鎖,這裏須要注意,目前不少文章認爲他是做爲替代OSSpinLock的方案就是自旋鎖是有問題的

  • void os_unfair_lock_lock(os_unfair_lock_t lock);:加鎖
  • bool os_unfair_lock_trylock(os_unfair_lock_t lock);:嘗試加鎖,成功返回true,失敗返回false
  • void os_unfair_lock_unlock(os_unfair_lock_t lock);:解鎖
  • void os_unfair_lock_assert_owner(os_unfair_lock_t lock);:若是當前線程未持有指定的鎖,則觸發斷言
  • void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock);:若是當前線程持有指定的鎖,則觸發斷言

各方法同常見的鎖沒太大差異,能夠看下方法註釋,只是須要注意一下初始化方式

{
    os_unfair_lock_t unfairLock;
    unfairLock = &(OS_UNFAIR_LOCK_INIT);
}
複製代碼

@synchronize(object)

@synchronized(object)指令使用傳入的對象做爲該鎖的惟一標識,只有當標識相同時,才知足互斥 @synchronized(object)指令實現鎖的優勢就是咱們不須要在代碼中顯式的建立鎖對象,即可以實現鎖的機制,並且不用擔憂忘記解鎖 使用方法極其常見,不作示例了

dispatch_semaphore

  • dispatch_semaphore_t dispatch_semaphore_create(long value);:建立信號量,傳入初始值
  • long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);:當信號<=0時,根據傳入的時間阻塞線程;若是信號>0則不阻塞線程,並對信號-1處理
  • long dispatch_semaphore_signal(dispatch_semaphore_t dsema);:對信號+1處理

GCD爲咱們提供的信號量也是經常使用的加鎖方式,常見用法是初始化信號值爲1

{
    dispatch_semaphore_t lock = dispatch_semaphore_create(1);

    dispatch_semaphore_wait(lock,DISPATCH_TIME_FOREVER);
    // 操做
    dispatch_semaphare_signal(lock);
}
複製代碼

常規操做你們都知道,有常規操做,那麼必定也有很是規操做,能夠看一下AFNetwork給咱們的示範

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}
複製代碼

AFURLSessionManager中,初始化使用dispatch_semaphore_create(0),在return tasks;前調用dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);阻塞線程,待block將目標值賦值後,執行dispatch_semaphore_signal(semaphore);,此時tasks已經有值,線程被喚醒後正常返回。很秀

pthread_mutex

C語言下的互斥鎖方案,是協議下四個類的底層

鎖經常使用函數:

  • pthread_mutex_init:動態初始化互斥量
  • PTHREAD_MUTEX_INITIALIZER:靜態建立互斥量
  • pthread_mutex_lock:給一個互斥量加鎖
  • pthread_mutex_trylock:加鎖,若是失敗不阻塞
  • pthread_mutex_unlock:解鎖
  • pthread_mutex_destroy:銷燬鎖

參數配置函數:

  • pthread_mutexattr_init:初始化參數
  • pthread_mutexattr_settype:設置類型
  • pthread_mutexattr_setpshared:設置做用域
  • pthread_mutexattr_destroy:銷燬參數

條件常見函數:

  • pthread_cond_init:動態初始化條件量
  • PTHREAD_COND_INITIALIZER:靜態建立條件量
  • pthread_cond_wait:傳入條件量及鎖
  • pthread_cond_signal:喚醒單條線程並加鎖
  • pthread_cond_broadcast:廣播喚醒全部線程
  • pthread_cond_destroy:銷燬條件

以上函數都是有返回值的,須要注意的是,若成功則返回0,不然返回錯誤編號,不是咱們習慣中的成功YES失敗NO

鎖類型:

  • PTHREAD_MUTEX_NORMAL:缺省值,這種類型的互斥鎖不會自動檢測死鎖。若是一個線程試圖對一個互斥鎖重複鎖定,將會引發這個線程的死鎖。若是試圖解鎖一個由別的線程鎖定的互斥鎖會引起不可預料的結果。若是一個線程試圖解鎖已經被解鎖的互斥鎖也會引起不可預料的結果
  • PTHREAD_MUTEX_ERRORCHECK:這種類型的互斥鎖會自動檢測死鎖。若是一個線程試圖對一個互斥鎖重複鎖定,將會返回一個錯誤代碼。若是試圖解鎖一個由別的線程鎖定的互斥鎖將會返回一個錯誤代碼。若是一個線程試圖解鎖已經被解鎖的互斥鎖也將會返回一個錯誤代碼
  • PTHREAD_MUTEX_RECURSIVE:若是一個線程對這種類型的互斥鎖重複上鎖,不會引發死鎖,一個線程對這類互斥鎖的屢次重複上鎖必須由這個線程來重複相同數量的解鎖,這樣才能解開這個互斥鎖,別的線程才能獲得這個互斥鎖。若是試圖解鎖一個由別的線程鎖定的互斥鎖將會返回一個錯誤代碼。若是一個線程試圖解鎖已經被解鎖的互斥鎖也將會返回一個錯誤代碼。這種類型的互斥鎖只能是進程私有的(做用域屬性PTHREAD_PROCESS_PRIVATE)
  • PTHREAD_MUTEX_DEFAULT:就是NORMAL類型

鎖做用域:

  • PTHREAD_PROCESS_PRIVATE:缺省值,做用域爲進程內
  • PTHREAD_PROCESS_SHARED:做用域爲進程間

使用示例:

static pthread_mutex_t c_lock;
- (void)testPthread_mutex {
    pthread_mutexattr_t c_lockAttr;
    pthread_mutexattr_init(&c_lockAttr);
    pthread_mutexattr_settype(&c_lockAttr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutexattr_setpshared(&c_lockAttr, PTHREAD_PROCESS_PRIVATE);
    
    pthread_mutex_init(&c_lock, &c_lockAttr);
    pthread_mutexattr_destroy(&c_lockAttr);

    pthread_t thread1;
    pthread_create(&thread1, NULL, _thread1, NULL);
    
    pthread_t thread2;
    pthread_create(&thread2, NULL, _thread2, NULL);
}

void *_thread1() {
    pthread_mutex_lock(&c_lock);
    printf("thread 1\n");
    pthread_mutex_unlock(&c_lock);
    return 0;
}

void *_thread2() {
    pthread_mutex_lock(&c_lock);
    printf("thread 2 busy\n");
    sleep(3);
    printf("thread 2\n");
    pthread_mutex_unlock(&c_lock);
    return 0;
}
複製代碼

使用鎖的注意點

  1. 互斥量須要時間來加鎖和解鎖。鎖住較少互斥量的程序一般運行得更快。因此,互斥量應該儘可能少,夠用便可,每一個互斥量保護的區域應則儘可能大。

  2. 互斥量的本質是串行執行。若是不少線程須要領繁地加鎖同一個互斥量, 則線程的大部分時間就會在等待,這對性能是有害的。若是互斥量保護的數據(或代碼)包含彼此無關的片斷,則能夠特大的互斥量分解爲幾個小的互斥量來提升性能。這樣,任意時刻須要小互斥量的線程減小,線程等待時間就會減小。因此,互斥量應該足夠多(到有意義的地步),每一個互斥量保護的區域則應儘可能的少。

參考文檔

Posix互斥量pthread_mutex_t iOS 常見知識點(三):Lock 再也不安全的 OSSpinLock How does @synchronized lock/unlock in Objective-C? [爆棧熱門 iOS 問題] atomic 和 nonatomic 有什麼區別? 《高性能iOS應用開發中文版》

相關文章
相關標籤/搜索