iOS線程安全的鎖與性能對比

iOS線程安全的鎖與性能對比

一、鎖的基本使用方法

1.1、@synchronized

這是我們最熟悉的枷鎖方式,用起來也比較簡單。使用時@synchronized后面需要緊跟一個OC對象,它實際上是把這個對象當做鎖來使用,作為該鎖的唯一標識,只有當標識相同時,才會滿足互斥。底層原理是當你調(diào)用sychronized的每個對象,runtime都會為其分配一個遞歸鎖并儲存在哈希表中。@synchronized原理

 NSObject *obj = [[NSObject alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @synchronized (obj) {
            NSLog(@"需要線程同步的操作1  開始");
            sleep(3);
            NSLog(@"需要線程同步的操作1  結(jié)束");
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        @synchronized (obj) {
            NSLog(@"需要線程同步的操作 2 ");
        }
    });

執(zhí)行結(jié)果很顯然,等第一個線程結(jié)束后,才執(zhí)行到第二個線程中

1.2、dispatch_semaphore

dispatch_semaphore是GCD用來同步的一種方式,與他相關的共有三個函數(shù),分別是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。

這個函數(shù)會使傳入的信號量dsema的值減1;如果dsema信號量的值大于0,該函數(shù)所處線程就繼續(xù)執(zhí)行下面的語句,并且將信號量的值減1;如果desema的值為0,那么這個函數(shù)就阻塞當前線程等待timeout(注意timeout的類型為dispatch_time_t,不能直接傳入整形或float型數(shù)),如果等待的期間desema的值被dispatch_semaphore_signal函數(shù)加1了,且該函數(shù)(即dispatch_semaphore_wait)所處線程獲得了信號量,那么就繼續(xù)向下執(zhí)行并將信號量減1。如果等待期間沒有獲取到信號量或者信號量的值一直為0,那么等到timeout時,其所處線程自動執(zhí)行其后語句。

dispatch_semaphore 是信號量,但當信號總量設為 1 時也可以當作鎖來。在沒有等待情況出現(xiàn)時,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現(xiàn)時,性能就會下降許多。相對于 OSSpinLock 來說,它的優(yōu)勢在于等待時不會消耗 CPU 資源。

如上的代碼,如果超時時間overTime設置成>2,可完成同步操作。如果overTime<2的話,在線程1還沒有執(zhí)行完成的情況下,此時超時了,將自動執(zhí)行下面的代碼。

 dispatch_semaphore_t signal = dispatch_semaphore_create(1);  // 信號量為1
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
   
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       
        dispatch_semaphore_wait(signal, overTime);  // 信號量減1 ; overtime 超時時間 過了超時時間不會等待
        NSLog(@"需要線程同步的操作1 開始");
        sleep(2);
        NSLog(@"需要線程同步的操作1 結(jié)束");
        dispatch_semaphore_signal(signal);
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        dispatch_semaphore_wait(signal, overTime);
        NSLog(@"需要線程同步的操作2  ");
        dispatch_semaphore_signal(signal);
    });

上面代碼的執(zhí)行結(jié)果:

2017-09-05 14:16:06.691 demo_lock[16669:271470] 需要線程同步的操作1 開始
2017-09-05 14:16:08.693 demo_lock[16669:271470] 需要線程同步的操作1 結(jié)束
2017-09-05 14:16:09.696 demo_lock[16669:271453] 需要線程同步的操作2

把overTime設置成<=2s 執(zhí)行結(jié)果為 :

2017-09-05 14:21:18.422 demo_lock[16746:280666] 需要線程同步的操作1 開始
2017-09-05 14:21:20.428 demo_lock[16746:280667] 需要線程同步的操作2
2017-09-05 14:21:20.428 demo_lock[16746:280666] 需要線程同步的操作1 結(jié)束

1.3、NSLock

NSLock 遵循 NSLocking 協(xié)議,lock 方法是加鎖,unlock 是解鎖,tryLock 是嘗試加鎖,如果失敗的話返回 NO,lockBeforeDate: 是在指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO。

  NSLock *lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"線程1  開始");
        sleep(2);
        NSLog(@"線程1  結(jié)束");
        [lock unlock];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1); // 保證線程二的代碼后執(zhí)行
        [lock lock]; // lock的第一秒 一直輪詢請求枷鎖, 后面階段會進入waiting狀態(tài)
        NSLog(@"線程2 開始");
        [lock unlock];
    });
    

控制臺輸出結(jié)果:
2017-09-05 15:08:33.782 demo_lock[17411:327912] 線程1 開始
2017-09-05 15:08:35.786 demo_lock[17411:327912] 線程1 結(jié)束
2017-09-05 15:08:35.786 demo_lock[17411:327911] 線程2 開始

線程 1 中的 lock 鎖上了,所以線程 2 中的 lock 加鎖失敗,阻塞線程 2,但 2 s 后線程 1 中的 lock 解鎖,線程 2 就立即加鎖成功,執(zhí)行線程 2 中的后續(xù)代碼。

查到的資料顯示互斥鎖會使得線程阻塞,阻塞的過程又分兩個階段,第一階段是會先空轉(zhuǎn),可以理解成跑一個 while 循環(huán),不斷地去申請加鎖,在空轉(zhuǎn)一定時間之后,線程會進入 waiting 狀態(tài),此時線程就不占用CPU資源了,等鎖可用的時候,這個線程會立即被喚醒。

所以如果將上面線程 1 中的 sleep(2); 改成 sleep(10); 輸出的結(jié)果會變成:

2017-09-05 15:09:30.271 demo_lock[17431:329738] 線程1 開始
2017-09-05 15:09:40.277 demo_lock[17431:329738] 線程1 結(jié)束
2017-09-05 15:09:40.277 demo_lock[17431:329740] 線程2 開始

NSLock只是在內(nèi)部封裝了一個pthread_mutex, 屬性為PTHREAD_MUTEX_ERRORCHECK

1.4、NSRecursiveLock遞歸鎖

遞歸鎖也是通過 pthread_mutex_lock 函數(shù)來實現(xiàn),在函數(shù)內(nèi)部會判斷鎖的類型。NSRecursiveLock 與 NSLock 的區(qū)別在于內(nèi)部封裝的 pthread_mutex_t 對象的類型不同,前者的類型為 PTHREAD_MUTEX_RECURSIVE。

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

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

2017-09-05 16:01:29.158 demo_lock[18197:370719] value = 5
2017-09-05 16:01:30.161 demo_lock[18197:370719] *** -[NSLock lock]: deadlock (<NSLock: 0x6000000cd120> '(null)')
2017-09-05 16:01:30.161 demo_lock[18197:370719] *** Break on _NSLockError() to debug.

在這種情況下,我們就可以使用NSRecursiveLock。它可以允許同一線程多次加鎖,而不會造成死鎖。遞歸鎖會跟蹤它被lock的次數(shù)。每次成功的lock都必須平衡調(diào)用unlock操作。只有所有達到這種平衡,鎖最后才能被釋放,以供其它線程使用。

我們將NSLock替換為NSRecursiveLock,上面代碼則會正確執(zhí)行

2017-09-05 16:02:56.521 demo_lock[18223:372981] value = 5
2017-09-05 16:02:57.526 demo_lock[18223:372981] value = 4
2017-09-05 16:02:58.531 demo_lock[18223:372981] value = 3
2017-09-05 16:02:59.531 demo_lock[18223:372981] value = 2
2017-09-05 16:03:00.537 demo_lock[18223:372981] value = 1

1.5、NSConditionLock條件鎖

當我們在使用多線程的時候,有時一把只會lock和unlock的鎖未必就能完全滿足我們的使用。因為普通的鎖只能關心鎖與不鎖,而不在乎用什么鑰匙才能開鎖,而我們在處理資源共享的時候,多數(shù)情況是只有滿足一定條件的情況下才能打開這把鎖:

  NSMutableArray *products = [NSMutableArray array];
    NSConditionLock *lock = [[NSConditionLock alloc] init];
    NSInteger HAS_DATA = 1;
    NSInteger NO_DATA = 0;
    
    dispatch_async(dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
       
        while (1) {
            [lock lockWhenCondition:NO_DATA];
            [products addObject:[[NSObject alloc] init]];
            NSLog(@"produce a product, 總量: %zi", products.count);
            [lock unlockWithCondition:HAS_DATA];
            sleep(1);
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        while (1) {
            NSLog(@"wait for product");
            [lock lockWhenCondition:HAS_DATA];
            [products removeObjectAtIndex:0];
            NSLog(@"custome a product");
            [lock unlockWithCondition:NO_DATA];
        }
    });

在線程1中的加鎖使用了lock,所以是不需要條件的,所以順利的就鎖住了,但在unlock的使用了一個整型的條件,它可以開啟其它線程中正在等待這把鑰匙的臨界地,而線程2則需要一把被標識為2的鑰匙,所以當線程1循環(huán)到最后一次的時候,才最終打開了線程2中的阻塞。但即便如此,NSConditionLock也跟其它的鎖一樣,是需要lock與unlock對應的,只是lock,lockWhenCondition:與unlock,unlockWithCondition:是可以隨意組合的,當然這是與你的需求相關的。

上面代碼執(zhí)行結(jié)果如下:
2017-09-05 16:55:21.740 demo_lock[19324:419225] wait for product
2017-09-05 16:55:21.740 demo_lock[19324:419224] produce a product, 總量: 1
2017-09-05 16:55:21.741 demo_lock[19324:419225] custome a product
2017-09-05 16:55:21.741 demo_lock[19324:419225] wait for product
2017-09-05 16:55:22.743 demo_lock[19324:419224] produce a product, 總量: 1
2017-09-05 16:55:22.743 demo_lock[19324:419225] custome a product
2017-09-05 16:55:22.744 demo_lock[19324:419225] wait for product

1.6、NSCondition條件鎖

 NSCondition *condition = [[NSCondition alloc] init];
    NSMutableArray *products = [NSMutableArray array];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        while (1) {
            [condition lock];
            if ([products count] == 0) {
                NSLog(@"wait for product");
                [condition wait];
            }
            [products removeObjectAtIndex:0];
            NSLog(@"custome a product");
            [condition unlock];
        }
        
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       
        while (1) {
            [condition lock];
            [products addObject:[[NSObject alloc] init]];
            NSLog(@"produce a product,總量:%zi",products.count);
            [condition signal];
            [condition unlock];
            sleep(1);
        }
        
    });

最基本的條件鎖。手動控制線程wait和signal

[condition lock];一般用于多線程同時訪問、修改同一個數(shù)據(jù)源,保證在同一時間內(nèi)數(shù)據(jù)源只被訪問、修改一次,其他線程的命令需要在lock 外等待,只到unlock ,才可訪問

[condition unlock];與lock 同時使用

[condition wait];讓當前線程處于等待狀態(tài)

[condition signal];CPU發(fā)信號告訴線程不用在等待,可以繼續(xù)執(zhí)行

上面代碼執(zhí)行結(jié)果如下:

2017-09-06 11:00:38.925 demo_lock[5145:119574] wait for product
2017-09-06 11:00:42.647 demo_lock[5145:119577] produce a product,總量:1
2017-09-06 11:00:55.847 demo_lock[5145:119574] custome a product
2017-09-06 11:01:05.752 demo_lock[5145:119574] wait for product
2017-09-06 11:01:08.262 demo_lock[5145:119577] produce a product,總量:1
2017-09-06 11:01:12.366 demo_lock[5145:119574] custome a product
2017-09-06 11:01:12.366 demo_lock[5145:119574] wait for product

1.7、pthread_mutex

c 語言定義下多線程枷鎖方式

1:pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t attr);
初始化鎖變量mutex。attr為鎖屬性,NULL值為默認屬性。
2:pthread_mutex_lock(
pthread_mutex_t* mutex);加鎖3:pthread_mutex_tylock(*pthread_mutex_t** *mutex);加鎖,但是與2不一樣的是當鎖已經(jīng)在使用的時候,返回為EBUSY,而不是掛起等待。4:pthread_mutex_unlock(pthread_mutex_t *mutex);釋放鎖5:pthread_mutex_destroy(pthread_mutex_t *mutex);使用完后釋放

   __block pthread_mutex_t theLock;
    pthread_mutex_init(&theLock, NULL);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       
        pthread_mutex_lock(&theLock);
        NSLog(@"需要線程同步的操作1 開始");
        sleep(4);
        NSLog(@"需要線程同步的操作1  結(jié)束");
        pthread_mutex_unlock(&theLock);
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        sleep(1);
        pthread_mutex_lock(&theLock);
        NSLog(@"需要線程同步的操作 2 ");
        
        pthread_mutex_unlock(&theLock);
    });

代碼執(zhí)行操作結(jié)果如下

2017-09-06 14:24:01.867 demo_lock[19902:324468] 需要線程同步的操作1 開始
2017-09-06 14:24:05.871 demo_lock[19902:324468] 需要線程同步的操作1 結(jié)束
2017-09-06 14:24:05.872 demo_lock[19902:324469] 需要線程同步的操作 2

1.8、pthread_mutex(recursive)遞歸鎖

    __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(&theLock, &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為了防止在遞歸的情況下出現(xiàn)死鎖而出現(xiàn)的遞歸鎖。作用和NSRecursiveLock遞歸鎖類似。

如果使用pthread_mutex_init(&theLock, NULL);初始化鎖的話,上面的代碼會出現(xiàn)死鎖現(xiàn)象。如果使用遞歸鎖的形式,則沒有問題。

打印臺輸出結(jié)果如下

2017-09-06 14:26:54.450 demo_lock[19975:329591] value = 5
2017-09-06 14:26:55.455 demo_lock[19975:329591] value = 4
2017-09-06 14:26:56.456 demo_lock[19975:329591] value = 3
2017-09-06 14:26:57.457 demo_lock[19975:329591] value = 2
2017-09-06 14:26:58.458 demo_lock[19975:329591] value = 1

二、鎖的性能對比

在 ibireme 的 不再安全的 OSSpinLock 一文中,有一張圖片簡單的比較了各種鎖的加解鎖性能:

性能對比

三、實現(xiàn)原理

自旋鎖的實現(xiàn)原理

自旋鎖的目的是為了確保臨界區(qū)只有一個線程可以訪問,偽代碼

do {
    Acquire Lock
        Critical section  // 臨界區(qū)
    Release Lock
        Reminder section // 不需要鎖保護的代碼
}

自旋鎖的實現(xiàn)思路很簡單,理論上來說只要定義一個全局變量,用來表示鎖的可用情況即可,偽代碼如下

bool lock = false; // 一開始沒有鎖上,任何線程都可以申請鎖
do {
    while(lock); // 如果 lock 為 true 就一直死循環(huán),相當于申請鎖
    lock = true; // 掛上鎖,這樣別的線程就無法獲得鎖
        Critical section  // 臨界區(qū)
    lock = false; // 相當于釋放鎖,這樣別的線程可以進入臨界區(qū)
        Reminder section // 不需要鎖保護的代碼        
}

這段代碼存在一個問題: 如果一開始有多個線程同時執(zhí)行 while 循環(huán),他們都不會在這里卡住,而是繼續(xù)執(zhí)行,這樣就無法保證鎖的可靠性了。解決思路也很簡單,只要確保申請鎖的過程是原子操作即可。

原子操作

狹義上:表示一條不可打斷的操作,在線程執(zhí)行操作過程中,不會被操作系統(tǒng)掛起,一定要執(zhí)行完畢

在多處理器的情況下,能夠被多個處理器同時執(zhí)行的操作不能算原子操作。因此,真正的原子操作必須由硬件提供支持。

我們只需要知道申請鎖的過程,可以用一個原子性操作test_and_set來完成 用偽代碼這樣表示

bool test_and_set (bool *target) {
    bool rv = *target; 
    *target = TRUE; 
    return rv;
}
自旋鎖的總結(jié)

原理

bool lock = false; // 一開始沒有鎖上,任何線程都可以申請鎖
do {
    while(test_and_set(&lock); // test_and_set 是一個原子操作
        Critical section  // 臨界區(qū)
    lock = false; // 相當于釋放鎖,這樣別的線程可以進入臨界區(qū)
        Reminder section // 不需要鎖保護的代碼        
}

如果臨界區(qū)的執(zhí)行時間過長,使用自旋鎖就不是個好主意 ;臨界區(qū)執(zhí)行時間較長,比如是文件讀寫,這種忙等是毫無必要的。

信號量
int sem_wait (sem_t *sem) {
  int *futex = (int *) sem;
  if (atomic_decrement_if_positive (futex) > 0)
    return 0;
  int err = lll_futex_wait (futex, 0);
    return -1;
)

首先會把信號量的值減一,并判斷是否大于0,如果大于0,說明不用等待,立刻返回。等待操作在lll_futex_wait函數(shù)中實現(xiàn)。使線程進入睡眠狀態(tài),主動讓出時間片,這個函數(shù)在互斥鎖的實現(xiàn)中,也有可能被用到。

主動讓出時間片并不代表效率高,會導致操作系統(tǒng)切換到另一個線程。上下文切換通常需要10ms,而且需要切換兩次。如果等待時間很短,比如只有幾微秒,忙等就比線程休眠更高效。

可以看出自旋鎖和信號量的實現(xiàn)都非常簡單,這也是兩者的加解鎖時間分別排在第一和第二的原因。

加解鎖耗時不能準確反應出鎖的效率,只能從一定程度上衡量鎖的實現(xiàn)負責程度。

pthread_mutex

phread表示POSIX thread,定義了一組跨平臺的線程相關的API,pthread_mutex表示互斥鎖?;コ怄i的實現(xiàn)原理與信號量非常相似,不是使用忙等,而是阻塞線程并睡眠,需要進行上下文切換。

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定義鎖的屬性

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) // 創(chuàng)建鎖

pthread_mutex_lock(&mutex); // 申請鎖
    // 臨界區(qū)
pthread_mutex_unlock(&mutex); // 釋放鎖

對于 pthread_mutex 來說,它的用法和之前沒有太大的改變,比較重要的是鎖的類型,可以有 PTHREAD_MUTEX_NORMALPTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE 等等,具體的特性就不做解釋了,網(wǎng)上有很多相關資料。

一般情況下,一個線程只能申請一次鎖,也只能在獲得鎖的情況下才能釋放鎖,多次申請鎖或釋放未獲得的鎖都會導致崩潰。假設在已經(jīng)獲得鎖的情況下再次申請鎖,線程會因為等待鎖的釋放而進入睡眠狀態(tài),因此就不可能再釋放鎖,從而導致死鎖。

然而這種情況經(jīng)常會發(fā)生,比如某個函數(shù)申請了鎖,在臨界區(qū)內(nèi)又遞歸調(diào)用了自己。辛運的是 pthread_mutex 支持遞歸鎖,也就是允許一個線程遞歸的申請鎖,只要把 attr 的類型改成 PTHREAD_MUTEX_RECURSIVE 即可。

互斥鎖的實現(xiàn)

互斥鎖在申請鎖時,調(diào)用了 pthread_mutex_lock 方法,它在不同的系統(tǒng)上實現(xiàn)各有不同,有時候它的內(nèi)部是使用信號量來實現(xiàn),即使不用信號量,也會調(diào)用到 lll_futex_wait 函數(shù),從而導致線程休眠。

上文說到如果臨界區(qū)很短,忙等的效率也許更高,所以在有些版本的實現(xiàn)中,會首先嘗試一定次數(shù)(比如 1000 次)的 test_and_test,這樣可以在錯誤使用互斥鎖時提高性能。

另外,由于 pthread_mutex 有多種類型,可以支持遞歸鎖等,因此在申請加鎖時,需要對鎖的類型加以判斷,這也就是為什么它和信號量的實現(xiàn)類似,但效率略低的原因。

NSLock

NSLock是Objective-C 已對象的形式暴露給開發(fā)者的一種鎖,實現(xiàn)簡單,通過宏,定義了lock方法:

#define    MLOCK \
- (void) lock\
{\
  int err = pthread_mutex_lock(&_mutex);\
  // 錯誤處理 ……
}

NSLock在內(nèi)部封裝了phread_mutex,屬性類型為PTHREAD_MUTEX_ERRORCHECK,它會損失一定性能換來錯誤提示。

NSLockpthread_mutex略慢的原因是在于它需要經(jīng)過方法調(diào)用,但是由于緩存的存在,多次方法調(diào)用不會對性能產(chǎn)生太大影響。

NSCondition

NSCondititon的底層是通過條件變量(condition variable)pthread_cond_t來實現(xiàn)的。條件變量有些像信號量,提供了線程阻塞與信號機制,因此可以用來阻塞某個線程,并等待某個數(shù)據(jù)就緒,隨后喚醒線程,比如常見的生產(chǎn)者-消費模式

很多介紹pthread_cond_t的文章都會提到,它需要與互斥鎖配合使用

void consumer () { // 消費者
    pthread_mutex_lock(&mutex);
    while (data == NULL) {
        pthread_cond_wait(&condition_variable_signal, &mutex); // 等待數(shù)據(jù)
    }
    // --- 有新的數(shù)據(jù),以下代碼負責處理 ↓↓↓↓↓↓
    // temp = data;
    // --- 有新的數(shù)據(jù),以上代碼負責處理 ↑↑↑↑↑↑
    pthread_mutex_unlock(&mutex);
}

void producer () {
    pthread_mutex_lock(&mutex);
    // 生產(chǎn)數(shù)據(jù)
    pthread_cond_signal(&condition_variable_signal); // 發(fā)出信號給消費者,告訴他們有了新的數(shù)據(jù)
    pthread_mutex_unlock(&mutex);
}
NSCondition的做法

NSCondition其實是封裝了一個互斥鎖和條件變量,把前者的lock方法和后者的wait/signal統(tǒng)一在NSCondition對象中,暴露給使用者:

- (void) signal {
  pthread_cond_signal(&_condition);
}

// 其實這個函數(shù)是通過宏來定義的,展開后就是這樣
- (void) lock {
  int err = pthread_mutex_lock(&_mutex);
}

NSRecursiveLock

上文已經(jīng)說過,遞歸鎖也是通過 pthread_mutex_lock 函數(shù)來實現(xiàn),在函數(shù)內(nèi)部會判斷鎖的類型,如果顯示是遞歸鎖,就允許遞歸調(diào)用,僅僅將一個計數(shù)器加一,鎖的釋放過程也是同理。

NSRecursiveLockNSLock 的區(qū)別在于內(nèi)部封裝的 pthread_mutex_t 對象的類型不同,前者的類型為 PTHREAD_MUTEX_RECURSIVE。

NSConditionLock

NSConditionLock借助NSCondition來實現(xiàn),它的本質(zhì)就是一個生產(chǎn)者-消費者模型?!皸l件被滿足”可以理解為生產(chǎn)者提供了新的內(nèi)容。NSConditionLock 的內(nèi)部持有一個 NSCondition 對象,以及 _condition_value 屬性,在初始化時就會對這個屬性進行賦值:

- (id) initWithCondition: (NSInteger)value {
    if (nil != (self = [super init])) {
        _condition = [NSCondition new]
        _condition_value = value;
    }
    return self;
}

它的lockWhenCondition方法其實就是消費者方法

- (void) lockWhenCondition: (NSInteger)value {
    [_condition lock];
    while (value != _condition_value) {
        [_condition wait];
    }
}

對應的unlockWhenCondition方法則是生產(chǎn)者,使用了broadcast方法通知了所有的消費者

- (void) unlockWithCondition: (NSInteger)value {
    _condition_value = value;
    [_condition broadcast];
    [_condition unlock];
}
@synchronized

這其實是一個 OC 層面的鎖, 主要是通過犧牲性能換來語法上的簡潔與可讀。

我們知道 @synchronized 后面需要緊跟一個 OC 對象,它實際上是把這個對象當做鎖來使用。這是通過一個哈希表來實現(xiàn)的,OC 在底層使用了一個互斥鎖的數(shù)組(你可以理解為鎖池),通過對對象去哈希值來得到對應的互斥鎖。

參考文獻

@synchronized原理
深入理解iOS開發(fā)中的鎖
不再安全的 OSSpinLock

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

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

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