iOS- 多線程中如何去保證線程安全

1、前言  

前段時間看了幾個開源項目,發現他們保持線程同步的方式各不相同,有@synchronized、NSLock、dispatch_semaphore、NSCondition、pthread_mutex、OSSpinLock。後來網上查了一下,發現他們的實現機制各不相同,性能也各不同。git

很差意思,咱們日常使用最多的@synchronized是性能最差的。github

 

2、介紹與使用

2.一、@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 結束");
        }
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        @synchronized(obj) {
            NSLog(@"須要線程同步的操做2");
        }
    });

 @synchronized(obj)指令使用的obj爲該鎖的惟一標識,只有當標識相同時,才爲知足互斥,若是線程2中的@synchronized(obj)改成   @synchronized(self),剛線程2就不會被阻塞,@synchronized指令實現鎖的優勢就是咱們不須要在代碼中顯式的建立鎖對象,即可以實現鎖的機制,但做爲一種預防措施,@synchronized塊會隱式的添加一個異常處理例程來保護代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖。因此若是不想讓隱式的異常處理例程帶來額外的開銷,你能夠考慮使用鎖對象。安全

上面結果的執行結果爲:多線程

2016-06-29 20:48:35.747 SafeMultiThread[35945:580107] 須要線程同步的操做1 開始
2016-06-29 20:48:38.748 SafeMultiThread[35945:580107] 須要線程同步的操做1 結束
2016-06-29 20:48:38.749 SafeMultiThread[35945:580118] 須要線程同步的操做2async

 

2.二、dispatch_semaphore  

   dispatch_semaphore_t signal = dispatch_semaphore_create(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);
            NSLog(@"須要線程同步的操做1 開始");
            sleep(2);
            NSLog(@"須要線程同步的操做1 結束");
        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);
    });

 

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

(1)dispatch_semaphore_create的聲明爲:性能

dispatch_semaphore_t dispatch_semaphore_create(long value);spa

傳入的參數爲long,輸出一個dispatch_semaphore_t類型且值爲value的信號量。線程

值得注意的是,這裏的傳入的參數value必須大於或等於0,不然dispatch_semaphore_create會返回NULL。debug

(2)dispatch_semaphore_signal的聲明爲:

long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

這個函數會使傳入的信號量dsema的值加1;

 (3) dispatch_semaphore_wait的聲明爲:

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

這個函數會使傳入的信號量dsema的值減1;這個函數的做用是這樣的,若是dsema信號量的值大於0,該函數所處線程就繼續執行下面的語句,而且將信號量的值減1;若是desema的值爲0,那麼這個函數就阻塞當前線程等待timeout(注意timeout的類型爲dispatch_time_t,不能直接傳入整形或float型數),若是等待的期間desema的值被dispatch_semaphore_signal函數加1了,且該函數(即dispatch_semaphore_wait)所處線程得到了信號量,那麼就繼續向下執行並將信號量減1。若是等待期間沒有獲取到信號量或者信號量的值一直爲0,那麼等到timeout時,其所處線程自動執行其後語句。

dispatch_semaphore 是信號量,但當信號總量設爲 1 時也能夠看成鎖來。在沒有等待狀況出現時,它的性能比 pthread_mutex 還要高,但一旦有等待狀況出現時,性能就會降低許多。相對於 OSSpinLock 來講,它的優點在於等待時不會消耗 CPU 資源。

如上的代碼,若是超時時間overTime設置成>2,可完成同步操做。若是overTime

上面代碼的執行結果爲:

2016-06-29 20:47:52.324 SafeMultiThread[35945:579032] 須要線程同步的操做1 開始
2016-06-29 20:47:55.325 SafeMultiThread[35945:579032] 須要線程同步的操做1 結束
2016-06-29 20:47:55.326 SafeMultiThread[35945:579033] 須要線程同步的操做2

若是把超時時間設置爲

2016-06-30 18:53:24.049 SafeMultiThread[30834:434334] 須要線程同步的操做1 開始
2016-06-30 18:53:25.554 SafeMultiThread[30834:434332] 須要線程同步的操做2
2016-06-30 18:53:26.054 SafeMultiThread[30834:434334] 須要線程同步的操做1 結束

 

2.三、NSLock  

    NSLock *lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //[lock lock];
        [lock lockBeforeDate:[NSDate date]];
            NSLog(@"須要線程同步的操做1 開始");
            sleep(2);
            NSLog(@"須要線程同步的操做1 結束");
        [lock unlock];

    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        if ([lock tryLock]) {//嘗試獲取鎖,若是獲取不到返回NO,不會阻塞該線程
            NSLog(@"鎖可用的操做");
            [lock unlock];
        }else{
            NSLog(@"鎖不可用的操做");
        }

        NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
        if ([lock lockBeforeDate:date]) {//嘗試在將來的3s內獲取鎖,並阻塞該線程,若是3s內獲取不到恢復線程, 返回NO,不會阻塞該線程
            NSLog(@"沒有超時,得到鎖");
            [lock unlock];
        }else{
            NSLog(@"超時,沒有得到鎖");
        }

    });

 

SLock是Cocoa提供給咱們最基本的鎖對象,這也是咱們常常所使用的,除lock和unlock方法外,NSLock還提供了tryLock和lockBeforeDate:兩個方法,前一個方法會嘗試加鎖,若是鎖不可用(已經被鎖住),剛並不會阻塞線程,並返回NO。lockBeforeDate:方法會在所指定Date以前嘗試加鎖,若是在指定時間以前都不能加鎖,則返回NO。

上面代碼的執行結果爲:

2016-06-29 20:45:08.864 SafeMultiThread[35911:575795] 須要線程同步的操做1 開始
2016-06-29 20:45:09.869 SafeMultiThread[35911:575781] 鎖不可用的操做
2016-06-29 20:45:10.869 SafeMultiThread[35911:575795] 須要線程同步的操做1 結束
2016-06-29 20:45:10.870 SafeMultiThread[35911:575781] 沒有超時,得到鎖

 

2.四、NSRecursiveLock遞歸鎖  

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

 

NSRecursiveLock實際上定義的是一個遞歸鎖,這個鎖能夠被同一線程屢次請求,而不會引發死鎖。這主要是用在循環或遞歸操做中。

這段代碼是一個典型的死鎖狀況。在咱們的線程中,RecursiveMethod是遞歸調用的。因此每次進入這個block時,都會去加一次鎖,而從第二次開始,因爲鎖已經被使用了且沒有解鎖,因此它須要等待鎖被解除,這樣就致使了死鎖,線程被阻塞住了。調試器中會輸出以下信息:

2016-06-30 19:08:06.393 SafeMultiThread[30928:449008] value = 5
2016-06-30 19:08:07.399 SafeMultiThread[30928:449008] -[NSLock lock]: deadlock ( ‘(null)’)
2016-06-30 19:08:07.399 SafeMultiThread[30928:449008] 
Break on _NSLockError() to debug.

在這種狀況下,咱們就可使用NSRecursiveLock。它能夠容許同一線程屢次加鎖,而不會形成死鎖。遞歸鎖會跟蹤它被lock的次數。每次成功的lock都必須平衡調用unlock操做。只有全部達到這種平衡,鎖最後才能被釋放,以供其它線程使用。

若是咱們將NSLock代替爲NSRecursiveLock,上面代碼則會正確執行。

2016-06-30 19:09:41.414 SafeMultiThread[30949:450684] value = 5
2016-06-30 19:09:42.418 SafeMultiThread[30949:450684] value = 4
2016-06-30 19:09:43.419 SafeMultiThread[30949:450684] value = 3
2016-06-30 19:09:44.424 SafeMultiThread[30949:450684] value = 2
2016-06-30 19:09:45.426 SafeMultiThread[30949:450684] value = 1

 

2.五、NSConditionLock條件鎖  

    NSMutableArray *products = [NSMutableArray array];

    NSInteger HAS_DATA = 1;
    NSInteger NO_DATA = 0;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        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];
        }

    });

 

當咱們在使用多線程的時候,有時一把只會lock和unlock的鎖未必就能徹底知足咱們的使用。由於普通的鎖只能關心鎖與不鎖,而不在意用什麼鑰匙才能開鎖,而咱們在處理資源共享的時候,多數狀況是隻有知足必定條件的狀況下才能打開這把鎖:

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

上面代碼執行結果以下:

2016-06-30 20:31:58.699 SafeMultiThread[31282:521698] wait for product
2016-06-30 20:31:58.699 SafeMultiThread[31282:521708] produce a product,總量:1
2016-06-30 20:31:58.700 SafeMultiThread[31282:521698] custome a product
2016-06-30 20:31:58.700 SafeMultiThread[31282:521698] wait for product
2016-06-30 20:31:59.705 SafeMultiThread[31282:521708] produce a product,總量:1
2016-06-30 20:31:59.706 SafeMultiThread[31282:521698] custome a product
2016-06-30 20:31:59.706 SafeMultiThread[31282:521698] wait for product
2016-06-30 20:32:00.707 SafeMultiThread[31282:521708] produce a product,總量:1
2016-06-30 20:32:00.708 SafeMultiThread[31282:521698] custome a product

2.六、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];通常用於多線程同時訪問、修改同一個數據源,保證在同一時間內數據源只被訪問、修改一次,其餘線程的命令須要在lock 外等待,只到unlock ,纔可訪問

[condition unlock];與lock 同時使用

[condition wait];讓當前線程處於等待狀態

[condition signal];CPU發信號告訴線程不用在等待,能夠繼續執行

上面代碼執行結果以下:

2016-06-30 20:21:25.295 SafeMultiThread[31256:513991] wait for product
2016-06-30 20:21:25.296 SafeMultiThread[31256:513994] produce a product,總量:1
2016-06-30 20:21:25.296 SafeMultiThread[31256:513991] custome a product
2016-06-30 20:21:25.297 SafeMultiThread[31256:513991] wait for product
2016-06-30 20:21:26.302 SafeMultiThread[31256:513994] produce a product,總量:1
2016-06-30 20:21:26.302 SafeMultiThread[31256:513991] custome a product
2016-06-30 20:21:26.302 SafeMultiThread[31256:513991] wait for product
2016-06-30 20:21:27.307 SafeMultiThread[31256:513994] produce a product,總量:1
2016-06-30 20:21:27.308 SafeMultiThread[31256:513991] custome a product

 

2.七、pthread_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(3);
            NSLog(@"須要線程同步的操做1 結束");
            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);

    });

 

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不同的是當鎖已經在使用的時候,返回爲EBUSY,而不是掛起等待。
4:pthread_mutex_unlock(pthread_mutex_t *mutex);釋放鎖
5:pthread_mutex_destroy(pthread_mutex_t* mutex);使用完後釋放

代碼執行操做結果以下:

2016-06-30 21:13:32.440 SafeMultiThread[31429:548869] 須要線程同步的操做1 開始
2016-06-30 21:13:35.445 SafeMultiThread[31429:548869] 須要線程同步的操做1 結束
2016-06-30 21:13:35.446 SafeMultiThread[31429:548866] 須要線程同步的操做2

 

2.八、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(&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爲了防止在遞歸的狀況下出現死鎖而出現的遞歸鎖。做用和NSRecursiveLock遞歸鎖相似。

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

 

2.九、OSSpinLock  

__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    NSLog(@"須要線程同步的操做1 開始");
    sleep(3);
    NSLog(@"須要線程同步的操做1 結束");
    OSSpinLockUnlock(&theLock);

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    sleep(1);
    NSLog(@"須要線程同步的操做2");
    OSSpinLockUnlock(&theLock);

});

 OSSpinLock 自旋鎖,性能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當等待時會消耗大量 CPU 資源,因此它不適用於較長時間的任務。 OSSpinLock已經再也不安全,請你們謹慎使用。

3、性能對比  

對以上各個鎖進行1000000此的加鎖解鎖的空操做時間以下:

OSSpinLock: 46.15 ms
dispatch_semaphore: 56.50 ms
pthread_mutex: 178.28 ms
NSCondition: 193.38 ms
NSLock: 175.02 ms
pthread_mutex(recursive): 172.56 ms
NSRecursiveLock: 157.44 ms
NSConditionLock: 490.04 ms
@synchronized: 371.17 ms

總的來講:

OSSpinLock和dispatch_semaphore的效率遠遠高於其餘。

@synchronized和NSConditionLock效率較差。

鑑於OSSpinLock的不安全,因此咱們在開發中若是考慮性能的話,建議使用dispatch_semaphore。

若是不考慮性能,只是圖個方便的話,那就使用@synchronized。

 

做者: 景銘巴巴

本文版權歸做者和博客園共有,歡迎轉載,但必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。清澈Saup.

相關文章
相關標籤/搜索