iOS 加鎖的方式


iOS多線程編程中,經常碰到多個線程訪問共同的一個資源,在線程相互交互的情況下,需要一些同步措施,來保證線程之間交互的時候是安全的。下面我們一起看一下學一下iOS的幾種常用的加鎖方式,希望對大家有所幫助!!!

  1. @synchronized
  2. NSLock對象鎖
  3. NSRecursiveLock遞歸鎖
  4. NSConditionLock條件鎖
  5. dispatch_semaphore 信號量實現加鎖(也就是GCD)
  6. OSSpinLock 與 os_unfair_lock
  7. pthread_mutex

 

介紹與使用

1.@synchronized

@synchronized關鍵字加鎖,互斥鎖,性能較差不推薦在項目中使用。

@synchronized(這里添加一個OC對象,一般使用self) {
       這里寫要加鎖的代碼
  }
注意點
1.加鎖的代碼要盡量少 2.添加的OC對象必須在多個線程中都是同一個對象 3.它的優點是不需要顯式的創建鎖對象,便可以實現鎖的機制。 4. @synchronized塊會隱式的添加異常處理例程來保護代碼,該處理例程會在異常拋出的時候就會自動  的釋放互斥鎖。如果不想讓隱式的異常處理例程帶來額外的開銷,你可以考慮使用鎖對象。

下面我們以一個最經典的例子:賣票

//設置票的數量為5
    _tickets = 5;
    
    //線程1
    dispatch_async(self.concurrentQueue, ^{
        [self saleTickets];
    });
    
    //線程2
    dispatch_async(self.concurrentQueue, ^{
        [self saleTickets];
    });
 
- (void)saleTickets
{
    while (1) {
        @synchronized(self) {
            [NSThread sleepForTimeInterval:1];
            if (_tickets > 0) {
                _tickets--;
                NSLog(@"剩余票數= %ld, Thread:%@",_tickets,[NSThread currentThread]);
            } else {
                NSLog(@"票賣完了  Thread:%@",[NSThread currentThread]);
                break;
            }
        }
    }
}

 

2.NSLock

基本所有鎖的接口都是通過NSLocking協議定義的,定義了lock和unlock方法,通過這些方法獲取和釋放鎖。NSLock是對mutex普通鎖的封裝

下面還是以賣票的例子講述一下。

//設置票的數量為5
    _tickets = 5;
    
    //創建鎖
    _mutexLock = [[NSLock alloc] init];
    
    //線程1
    dispatch_async(self.concurrentQueue, ^{
        [self saleTickets];
    });
    
    //線程2
    dispatch_async(self.concurrentQueue, ^{
        [self saleTickets];
    });
 
- (void)saleTickets
{
 
    while (1) {
        [NSThread sleepForTimeInterval:1];
        //加鎖
        [_mutexLock lock];
        if (_tickets > 0) {
            _tickets--;
            NSLog(@"剩余票數= %ld, Thread:%@",_tickets,[NSThread currentThread]);        
        } else {
            NSLog(@"票賣完了  Thread:%@",[NSThread currentThread]);
            break;
        }
        //解鎖
        [_mutexLock unlock];
    }
}

 

3.NSRecursiveLock遞歸鎖

使用鎖比較容易犯的錯誤是在遞歸或者循環中造成死鎖。

如下代碼鎖會被多次lock,造成自己被阻塞。

//創建鎖
    _mutexLock = [[NSLock alloc]init];
  
    //線程1
    dispatch_async(self.concurrentQueue, ^{
        static void(^TestMethod)(int);
        TestMethod = ^(int value)
        {
            [_mutexLock lock];
            if (value > 0)
            {
                [NSThread sleepForTimeInterval:1];
                TestMethod(value--);
            }
            [_mutexLock unlock];
        };
        
        TestMethod(5);
    });

如果把這個NSLock換成NSRecursiveLock,就可以解決問題。

NSRecursiveLock類定義的鎖,可以在同一線程多次lock,不會造成死鎖。

//創建鎖
    _rsLock = [[NSRecursiveLock alloc] init];
    
   //線程1
    dispatch_async(self.concurrentQueue, ^{
        static void(^TestMethod)(int);
        TestMethod = ^(int value)
        {
            [_rsLock lock];
            if (value > 0)
            {
                [NSThread sleepForTimeInterval:1];
                TestMethod(value--);
            }
            [_rsLock unlock];
        };
        
        TestMethod(5);
    });

 

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

在線程1中的加鎖使用了lock,所以是不要條件的,也就鎖住了。但在unlock的使用整型條件,它可以開啟其他線程中正在等待鑰匙的臨界池,當線程1循環到一次的時候,打開了線程2的阻塞。

NSCoditionLock中lock,lockWhenCondition:與unlock,unlockWithCondition:是可以隨意組合的,具體使用根據需求來區分。

NSCoditionLock 是對NSCodition的進一步封裝,可以設置具體的條件值,而NSCodition是對mutex和cond的封裝---看本篇博客7.3 條件鎖

 

5.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_wait,dispatch_semaphore_signal,dispatch_semaphore_create。

(1)dispatch_semaphore_create的聲明為:

dispatch_semaphore_t dispatch_semaphore_create(long value);

傳入的參數是long類型,輸出一個dispatch_semaphore_t類型值為Value的信號量(value傳入值不能小於0,否則會報錯NULL)

(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;如果dsema為0時,函數就會阻塞當前的線程,如果等待的期間發現dsema的值被dispatch_semaphore_signal加1了,並且該函數得到了信號量,那么繼續向下執行,並將信號量減1,如果等待期間沒有獲得信號量或者值一直為0,那么等到timeout,所處的線程也會自動執行下面的代碼。

dispatch_semaphore,當信號量為1時,可以作為鎖使用。如果沒有出現等待的情況,它的性能比pthread_mutex還要高,當如果有等待情況的時候,性能就會下降很多,相比OSSpinLock(暫不講解),它的優勢在於等待的時侯不會消耗CPU資源。

針對上面代碼,發現如果超時時間overTime>2,可完成同步操作,反之,在線程1還沒有執行完的情況下,此時超時了,將自動執行下面的代碼。

上面代碼執行結果:

2018-09-18 15:40:52.324 SafeMultiThread[35945:579032] 需要線程同步的操作1 開始  
2018-09-18 15:40:52.325 SafeMultiThread[35945:579032] 需要線程同步的操作1 結束  
2018-09-18 15:40:52.326 SafeMultiThread[35945:579033] 需要線程同步的操作2  

如果將overTime<2s的時候,執行為

2018-09-18 15:40:52.049 SafeMultiThread[30834:434334] 需要線程同步的操作1 開始  
2018-09-18 15:40:52.554 SafeMultiThread[30834:434332] 需要線程同步的操作2  
2018-09-18 15:40:52.054 SafeMultiThread[30834:434334] 需要線程同步的操作1 結束  

 

6.OSSpinLock自旋鎖與os_unfair_lock

6.1   OSSpinLock

OSSpinLock叫做“自旋鎖”,自旋鎖的線程會處於忙等(busy-wait)狀態,一直占用着CPU資源

但是目前是不再安全了,在iOS 10之后棄用啦,如果等待鎖的線程優先級較高,它會一直占用着CPU資源,優先級低的線程就無法釋放鎖。

在使用的過程中需要導入頭文件#import <libkern/OSAtomic.h>

//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//嘗試加鎖(如果需要等待就不加鎖,直接返回false;如果不需要等待就加鎖,返回True)
bool result = OSSpinLockTry(&lock);
//加鎖
OSSpinLockLock(&lock);
//解鎖
OSSpinLockUnlock(&lock)

 

6.2 os_unfair_lock互斥鎖

os_unfair_lock用於取代不安全的OSSpinLock,從iOS10開始才支持。從底層調用看,等待os_unfair_lock鎖的線程會處於休眠狀態,並非忙等

os_unfair_lock需要導入頭文件#import<os/lock.h>

    //初始化
    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    //嘗試加鎖
    os_unfair_lock_trylock(&lock);
    //加鎖
    os_unfair_lock_lock(&lock);
    //解鎖
    os_unfair_lock_unlock(&lock);

 

7.pthread_mutex

mutex叫做“互斥鎖”,等待鎖的線程處於休眠狀態

需要導入頭文件#import <pthread.h>

7.1  

    //初始化鎖的屬性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    
    //初始化鎖
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &attr);
    
    //嘗試加鎖
    pthread_mutex_trylock(&mutex);
    
    //加鎖
    pthread_mutex_lock(&mutex);
    
    //解鎖
    pthread_mutex_unlock(&mutex);
    
    //銷毀相關資源 --pthread_mutex在對象類釋放的時候要銷毀,其他鎖無此情況
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_destroy(&mutex);

7.2 pthread_mutex-遞歸鎖

     //初始化鎖的屬性
    pthread_mutexattr_t attr;
    pthread_mutexattr_t_int(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);//遞歸鎖
    
    //初始化鎖
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &attr);

7.3 pthread_mutex-條件

#import "MutexDemo3.h"

#import <pthread.h>

@interface MutexDemo3()
@property (assign, nonatomic) pthread_mutex_t mutex;
@property (assign, nonatomic) pthread_cond_t cond;
@property (strong, nonatomic) NSMutableArray *data;
@end

@implementation MutexDemo3

- (instancetype)init
{
    if (self = [super init]) {
        // 初始化屬性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        // 初始化鎖
        pthread_mutex_init(&_mutex, &attr);
        // 銷毀屬性
        pthread_mutexattr_destroy(&attr);
        
        // 初始化條件
        pthread_cond_init(&_cond, NULL);
        
        self.data = [NSMutableArray array];
    }
    return self;
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 生產者-消費者模式

// 線程1
// 刪除數組中的元素
- (void)__remove
{
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"刪除了元素");
    
    pthread_mutex_unlock(&_mutex);
}

// 線程2
// 往數組中添加元素
- (void)__add
{
    pthread_mutex_lock(&_mutex);
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 信號
    pthread_cond_signal(&_cond);
    // 廣播
//    pthread_cond_broadcast(&_cond);
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc
{
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
}

@end

 

補充:自旋鎖、互斥鎖比較

1. 什么情況下使用自旋鎖比較划算?(OSSpinLock,但被os_unfair_lock取代,但是os_unfair_lock不是自旋鎖)

  • 預計線程等待鎖的時間很短
  • 加鎖的代碼(臨界區)經常被調用,但競爭情況很少發生
  • CPU資源不緊張
  • 多核處理器

2. 什么情況下使用互斥鎖比較划算?(pthread_mutex, NSLock等)

  • 預計線程等待鎖的時間較長
  • 單核處理器
  • 臨界區有IO操作
  • 臨界區代碼復雜或者循環量大
  • 臨界區競爭非常激烈

 

以上就是自己在開發中所經常使用到的加鎖方式,希望對大家有所幫助!!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM