OC 各種執行緒鎖

韋家冰發表於2017-12-13

參考: 正確使用多執行緒同步鎖@synchronized() iOS中的鎖 iOS多執行緒安全詳解 iOS 常見知識點(三):Lock

#####各種鎖

1、 簡單常見的: (1)atomic (2)@synchronized 2、NS物件形式的: (1)NSLock (2)NSConditionLock條件鎖 (3)NSRecursiveLock遞迴鎖 (4)NSCondition掛起喚醒 3、GCD的: (1)dispatch_semaphore (2)dispatch_barrier_async 4、高階的: (1)OSSpinLock (2)os_unfair_lock (3)pthread_mutex_t

效能對比

#####atomic atomic:原子屬性,只是為gettet、setter方法加鎖 atomic與nonatomic的區別

   // nonatomic修飾,會崩潰
    for (int i=0; i< 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.obj_nonatomic = [NSObject new];
        });
    }
    
    // atomic修飾,不崩潰
    for (int i=0; i< 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.obj_atomic = [NSObject new];
        });
    }
複製程式碼

atomic只是gettet、setter方法加鎖,其他操作會有執行緒安全問題

@interface ViewController ()
@property (atomic , strong) NSString *info;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            self.info = @"a";
            NSLog(@"A--info:%@", self.info);
        }
    });

    //B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            self.info = @"b";
            NSLog(@"B--info:%@", self.info);
        }
    });    
    // 根據執行緒安全定義,如果atomic為執行緒安全A輸出應該永遠為A--info:a,B輸出應該永遠為B--info:b
    // NSlog會有:A--info:b
}
@end
複製程式碼

#####@synchronized(obj)

  • synchronized是使用的遞迴mutex來做同步。
  • @synchronized(nil)不起任何作用

synchronized中傳入的object的記憶體地址,被用作key,通過hash map對應的一個系統維護的遞迴鎖。 所以不管是傳入什麼型別的object,只要是有記憶體地址,就能啟動同步程式碼塊的效果。

注意:需要合理使用obj,不是全部都用self

// 簡單用法
@synchronized(obj) {
  //code
}

// 可以巢狀、遞迴
@synchronized(obj) {
      @synchronized(obj) {
      //code
    }
}
複製程式碼

#####NSLocking NSLocking協議定義了兩個例項方法,lock和unlock對應著加鎖與解鎖

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end
複製程式碼

NSLock、NSConditionLock、NSRecursiveLock、NSCondition對應的例項都可以通過lock/unlock來進行加鎖/解鎖。

######NSLock

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

// 嘗試加鎖,如果失敗了,並不會阻塞執行緒,只是立即返回NO
- (BOOL)tryLock; 
// 是在指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO,阻塞執行緒。
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end
複製程式碼

簡單測試例子:

    //主執行緒中
    NSLock *lock = [[NSLock alloc] init];
    //執行緒1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"執行緒1");
        [lock unlock];
        NSLog(@"執行緒1解鎖成功");
    });
    
    //執行緒2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"執行緒2");
        [lock unlock];
        NSLog(@"執行緒2解鎖成功");
    });
複製程式碼

######NSConditionLock條件鎖

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;

- (void)lockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;

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

- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end
複製程式碼

condition實現條件鎖時(也可以不實現,直接呼叫協議方法lock),只有符合條件才能上鎖,但是解鎖為非條件,任意condition都可以解鎖,此時設定的condition為下一次條件鎖的condition。

虛擬碼

- (instancetype)initWithCondition:(NSInteger)condition {
        if (self =[ [NSConditionLock alloc] init]) {
               _condition = condition
        }
        return self;
}
// 利用condition加鎖、解鎖時虛擬碼是這樣的
- (void)lockWhenCondition:(NSInteger)condition {
        if (_condition == condition) [self lock];
}
- (void)unlockWithCondition:(NSInteger)condition {
      [self setValue:@condition forKey:@"condition"];
      [self unlock];
}
複製程式碼

簡單例子:

    //主執行緒中
    NSConditionLock *theLock = [[NSConditionLock alloc] init];
    
    //執行緒1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i=0;i<=2;i++)
        {
            [theLock lock];
            NSLog(@"thread1:%d",i);
            sleep(2);
            [theLock unlockWithCondition:i];
        }
    });
    
    //執行緒2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [theLock lockWhenCondition:2];
        NSLog(@"thread2");
        [theLock unlock];
    });
    
    /*執行結果
     2017-03-04 22:21:29.031 LockDemo[87455:3031878] thread1:0
     2017-03-04 22:21:31.105 LockDemo[87455:3031878] thread1:1
     2017-03-04 22:21:33.175 LockDemo[87455:3031878] thread1:2
     2017-03-04 22:21:35.249 LockDemo[87455:3031879] thread2
     */
複製程式碼

######NSRecursiveLock遞迴鎖 NSRecursiveLock 是遞迴鎖,他和 NSLock 的區別在於,NSRecursiveLock 可以在一個執行緒中重複加鎖(反正單執行緒內任務是按順序執行的,不會出現資源競爭問題),NSRecursiveLock 會記錄上鎖和解鎖的次數,當二者平衡的時候,才會釋放鎖,其它執行緒才可以上鎖成功。

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

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

@property (nullable, copy) NSString *name ;

@end
複製程式碼

簡單例子

static int i = 10;

- (void)recursiveLock {
    [_lock lock];
    NSLog(@"NSRecursiveLock--%zd", i--);
    if (i >= 0) {
        [self recursiveLock];
    }
    [_lock unlock];
}
複製程式碼

######NSCondition NSCondition中有這些方法

- (void)wait; //掛起執行緒
- (BOOL)waitUntilDate:(NSDate *)limit; //什麼時候掛起執行緒
- (void)signal; // 喚醒一條掛起執行緒
- (void)broadcast; //喚醒所有掛起執行緒
複製程式碼

使用例子:

    NSCondition *_lock = [NSCondition new];
    //A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [_lock lock];
        NSLog(@"A執行緒加鎖");
        [_lock wait];
        NSLog(@"A執行緒喚醒");
        [_lock unlock];
        NSLog(@"A執行緒解鎖");
    });
    
    //B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [_lock lock];
        NSLog(@"B執行緒加鎖");
        [_lock wait];
        NSLog(@"B執行緒喚醒");
        [_lock unlock];
        NSLog(@"B執行緒解鎖");
    });
    
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        [_lock signal];// 喚醒一條執行緒
    });
    
    /*執行結果
     2017-10-20 18:00:59.848350+0800 debug-objc[41375:32423729] A執行緒加鎖
     2017-10-20 18:00:59.848624+0800 debug-objc[41375:32423730] B執行緒加鎖
     2017-10-20 18:01:01.853165+0800 debug-objc[41375:32423729] A執行緒喚醒
     2017-10-20 18:01:01.853236+0800 debug-objc[41375:32423729] A執行緒解鎖
     */
複製程式碼
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        [_lock broadcast];// 喚醒全部
    });
    
    /*執行結果
     2017-10-20 18:02:58.031822+0800 debug-objc[41428:32430448] A執行緒加鎖
     2017-10-20 18:02:58.033322+0800 debug-objc[41428:32430447] B執行緒加鎖
     2017-10-20 18:03:00.036548+0800 debug-objc[41428:32430448] A執行緒喚醒
     2017-10-20 18:03:00.037160+0800 debug-objc[41428:32430448] A執行緒解鎖
     2017-10-20 18:03:00.037411+0800 debug-objc[41428:32430447] B執行緒喚醒
     2017-10-20 18:03:00.037532+0800 debug-objc[41428:32430447] B執行緒解鎖
     */
複製程式碼

#####GCD的dispatch_semaphore訊號量

/*! 
 * @param value 訊號量的起始值,當傳入的值小於零時返回NULL
 * @result 成功返回一個新的訊號量,失敗返回NULL
 */
dispatch_semaphore_t dispatch_semaphore_create(long value)

/*!
 * @discussion 訊號量減1,如果結果小於0,那麼等待佇列中訊號增量到來直到timeout
 * @param dsema 訊號量
 * @param timeout 等待時間,型別為dispatch_time_t,這裡有兩個巨集DISPATCH_TIME_NOW、DISPATCH_TIME_FOREVER
 * @result 若等待成功返回0,timeout返回非0
 */
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

/*!
 * @discussion 訊號量加1,如果之前的訊號量小於0,將喚醒一條等待執行緒
 * @param dsema  訊號量
 * @result 喚醒一條執行緒返回非0,否則返回0
 */
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

複製程式碼

簡單例子

- (void)semaphore {
    dispatch_semaphore_t dsema = dispatch_semaphore_create(1);

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER);
            _info = @"a";
            NSLog(@"A--info:%@", _info);
            dispatch_semaphore_signal(dsema);
        }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER);
            _info = @"b";
            NSLog(@"B--info:%@", _info);
            dispatch_semaphore_signal(dsema);
        }
    });
}
複製程式碼

#####GCD中“柵欄函式”:dispatch_barrier_async

    //同dispatch_queue_create函式生成的concurrent Dispatch Queue佇列一起使用
    dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"----1");
    });
    dispatch_async(queue, ^{
        NSLog(@"----2");
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"----3");
    });
    dispatch_async(queue, ^{
        NSLog(@"----4");
    });
複製程式碼

上述程式碼列印結果總是1 2 –> barrier –>3 4,即1、2總在barrier之前列印,3、4總在barrier之後列印,其中1、2 由於並行處理先後順序不定,當然3、4也一樣。

#####OSSpinLock OSSpinLock自旋鎖,使用時需匯入標頭檔案#import <libkern/OSAtomic.h> 由於自旋鎖存在優先順序反轉問題(可檢視YYKit作者的這篇文章 不再安全的 OSSpinLock),在iOS 10.0中被<os/lock.h>中的os_unfair_lock()取代

    // 初始化 unlock為0,lock為非0
    OSSpinLock spinLock = OS_SPINLOCK_INIT;
    // 加鎖
    OSSpinLockLock(&spinLock);
    // 解鎖
    OSSpinLockUnlock(&spinLock);
    // 嘗試加鎖
    BOOL b = OSSpinLockTry(&spinLock);
複製程式碼
- (void)OSSpinLock {

    OSSpinLock spinLock = OS_SPINLOCK_INIT;
    NSLog(@"加鎖前:%zd", spinLock);
    OSSpinLockLock(&spinLock);
    NSLog(@"加鎖後:%zd", spinLock);
    OSSpinLockUnlock(&spinLock);
    NSLog(@"解鎖後:%zd", spinLock);
    /*執行結果
     2017-10-20 18:36:40.237586+0800 debug-objc[41870:32523780] 加鎖前:0
     2017-10-20 18:36:40.237904+0800 debug-objc[41870:32523780] 加鎖後:4294967295
     2017-10-20 18:36:40.237955+0800 debug-objc[41870:32523780] 解鎖後:0
     */
}
複製程式碼

#####os_unfair_lock os_unfair_lock iOS 10.0新推出的鎖,用於解決OSSpinLock優先順序反轉問題(用法與OSSpinLock差不多)

    // 初始化
    os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
    // 加鎖
    os_unfair_lock_lock(unfairLock);
    // 解鎖
    os_unfair_lock_unlock(unfairLock);
    // 嘗試加鎖
    BOOL b = os_unfair_lock_trylock(unfairLock);

複製程式碼

######POSIX LOCK POSIX LOCK為C語言級別的鎖,需引入頭像檔案#import<pthread.h> ######pthread_mutex_t

pthread_mutex_init(&lock, NULL);

PTHREAD_MUTEX_NORMAL 預設型別,也就是普通鎖。當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後先進先出原則獲得鎖。

PTHREAD_MUTEX_ERRORCHECK 檢錯鎖,如果同一個執行緒請求同一個鎖,則返回 EDEADLK,否則與普通鎖型別動作相同。這樣就保證當不允許多次加鎖時不會出現巢狀情況下的死鎖。

PTHREAD_MUTEX_RECURSIVE 遞迴鎖,允許同一個執行緒對同一個鎖成功獲得多次,並通過多次 unlock 解鎖。

PTHREAD_MUTEX_DEFAULT 適應鎖,動作最簡單的鎖型別,僅等待解鎖後重新競爭,沒有等待佇列。
複製程式碼
static pthread_mutex_t lock;
- (void)pLock {

    pthread_mutex_init(&lock, NULL);

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            pthread_mutex_lock(&lock);
            _info = @"a";
            NSLog(@"A--info:%@", _info);
            pthread_mutex_unlock(&lock);
        }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            pthread_mutex_lock(&lock);
            _info = @"b";
            NSLog(@"B--info:%@", _info);
            pthread_mutex_unlock(&lock);
        }
    });
}
複製程式碼

相關文章