iOS中的“鎖事”

Inlight發表於2017-12-21

拋磚引玉

說到鎖不得不提執行緒安全,說到執行緒安全,作為iOS程式設計師又不得不提 nonatomicatomic

  • nonatomic 不會對生成的 gettersetter 方法加同步鎖(非原子性)
  • atomic 會對生成的 gettersetter 加同步鎖(原子性)

setter / getteratomic 修飾的屬性時,該屬性是讀寫安全的。然而讀寫安全並不代表執行緒安全。

執行緒安全概念(thread safety)

  • 執行緒安全就是多執行緒訪問時,採用了加鎖機制,當一個執行緒訪問該類的某個資料時,進行保護,其他執行緒不能進行訪問直到該執行緒讀取完,其他執行緒才可使用。不會出現資料不一致或者資料汙染。
  • 執行緒不安全就是不提供資料訪問保護,有可能出現多個執行緒先後更改資料造成所得到的資料是髒資料。

驗證 atomic 非執行緒安全

  • 驗證程式碼
#import "ViewController.h"

@interface ViewController ()

@property (strong, atomic) NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //atomic非執行緒安全驗證
    //Jack
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            self.name = @"Jack";
            NSLog(@"Jack is %@", self.name);
        }
    });
    
    //Rose
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            self.name = @"Rose";
            NSLog(@"Rose is %@", self.name);
        }
    });
}
複製程式碼
  • 驗證結果
2017-11-29 11:21:27.713446+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.713487+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.713638+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.713659+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.713840+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.714050+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.714205+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.718069+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.718069+0800 LockDemo[42637:1199500] Jack is Rose
2017-11-29 11:21:27.718199+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.718199+0800 LockDemo[42637:1199499] Rose is Jack
複製程式碼

最後一行和倒數第三行可以看到,atomic 非執行緒安全驗證完畢。

  • 也就是說 atomic 只能做到讀寫安全並不能做到執行緒安全,若要實現執行緒安全還需要採用更為深層的鎖定機制才行。
  • iOS開發時一般都會使用 nonatomic 屬性,因為在iOS中使用同步鎖的開銷較大,這會帶來效能問題,但是在Mac OS X程式時,使用 atomic 屬性通常都不會有效能瓶頸。

鎖的概念

在電腦科學中,鎖是一種同步機制,用於在存在多執行緒的環境中實施對資源的訪問限制。

鎖的作用

  • 通俗來講:就是為了防止在多執行緒的情況下對共享資源的髒讀或者髒寫。
  • 也可以理解為:執行多執行緒時用於強行限制資源訪問的同步機制,即併發控制中保證互斥的要求。

iOS開發中常用的鎖

  • @synchronized
  • NSLock 物件鎖
  • NSRecursiveLock 遞迴鎖
  • NSConditionLock 條件鎖
  • pthread_mutex 互斥鎖(C語言)
  • dispatch_semaphore 訊號量實現加鎖(GCD
  • OSSpinLock 自旋鎖

效能圖來源:ibireme

@synchronized

@synchronized 其實是一個 OC 層面的鎖, 主要是通過犧牲效能換來語法上的簡潔與可讀性。 @synchronized 是我們平常使用最多的但是效能最差的。 OC寫法:

@synchronized(self) {
    //需要執行的程式碼塊
}
複製程式碼

swift寫法:

objc_sync_enter(self)
//需要執行的程式碼塊
objc_sync_exit(self)
複製程式碼

程式碼示例:

    //執行緒1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(self) {
            NSLog(@"第一個執行緒同步操作開始");
            sleep(3);
            NSLog(@"第一個執行緒同步操作結束");
        }
    });
    //執行緒2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        @synchronized(self) {
            NSLog(@"第二個執行緒同步操作");
        }
    });
複製程式碼

結果:

2017-11-29 14:36:52.056457+0800 LockDemo[46145:1306472] 第一個執行緒同步操作開始
2017-11-29 14:36:55.056868+0800 LockDemo[46145:1306472] 第一個執行緒同步操作結束
2017-11-29 14:36:55.057261+0800 LockDemo[46145:1306473] 第二個執行緒同步操作
複製程式碼
  • @synchronized(self) 指令使用的 self 為該鎖的唯一標識,只有當標識相同時,才為滿足互斥,如果執行緒2中的 self 改成其它物件,執行緒2就不會被阻塞。
    NSString *s = [NSString string];
    //執行緒1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized(self) {
            NSLog(@"第一個執行緒同步操作開始");
            sleep(3);
            NSLog(@"第一個執行緒同步操作結束");
        }
    });
    //執行緒2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        @synchronized(s) {
            NSLog(@"第二個執行緒同步操作");
        }
    });
複製程式碼
2017-11-29 14:43:54.930414+0800 LockDemo[46287:1312173] 第一個執行緒同步操作開始
2017-11-29 14:43:55.930761+0800 LockDemo[46287:1312158] 第二個執行緒同步操作
2017-11-29 14:43:57.932287+0800 LockDemo[46287:1312173] 第一個執行緒同步操作結束
複製程式碼
  • @synchronized 指令實現鎖的優點就是我們不需要在程式碼中顯式的建立鎖物件,便可以實現鎖的機制,但作為一種預防措施,@synchronized 塊會隱式的新增一個異常處理來保護程式碼,該處理會在異常丟擲的時候自動的釋放互斥鎖。所以如果不想讓隱式的異常處理例程帶來額外的開銷,你可以考慮使用鎖物件。

NSLock

  • NSLock 中實現了一個簡單的互斥鎖。通過 NSLocking 協議定義了 lockunlock 方法。
@protocol NSLocking

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

@end
複製程式碼

舉個栗子賣冰棍兒

- (void)nslockTest {
    //設定冰棍兒的數量為5
    _count = 5;
    
    //建立鎖
    _lock = [[NSLock alloc] init];
    
    //執行緒1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saleIceCream];
    });
    
    //執行緒2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saleIceCream];
    });
}

- (void)saleIceCream
{
    while (1) {
        sleep(1);
        //加鎖
        [_lock lock];
        if (_count > 0) {
            _count--;
            NSLog(@"剩餘冰棍兒數= %ld, Thread - %@", _count, [NSThread currentThread]);
        } else {
            NSLog(@"冰棍兒賣光光  Thread - %@",[NSThread currentThread]);
            break;
        }
        //解鎖
        [_lock unlock];
    }
}
複製程式碼

加鎖結果:

2017-11-29 16:21:29.728198+0800 LockDemo[55262:1411318] 剩餘冰棍兒數= 4, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:29.728428+0800 LockDemo[55262:1411319] 剩餘冰棍兒數= 3, Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}
2017-11-29 16:21:30.729009+0800 LockDemo[55262:1411318] 剩餘冰棍兒數= 2, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:30.729378+0800 LockDemo[55262:1411319] 剩餘冰棍兒數= 1, Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}
2017-11-29 16:21:31.733061+0800 LockDemo[55262:1411318] 剩餘冰棍兒數= 0, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:31.733454+0800 LockDemo[55262:1411319] 冰棍兒賣光光  Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}
複製程式碼

不加鎖結果:

2017-11-29 16:23:38.702352+0800 LockDemo[55316:1412917] 剩餘冰棍兒數= 3, Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:38.702352+0800 LockDemo[55316:1412919] 剩餘冰棍兒數= 4, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:39.705096+0800 LockDemo[55316:1412919] 剩餘冰棍兒數= 2, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:39.705099+0800 LockDemo[55316:1412917] 剩餘冰棍兒數= 1, Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:40.709617+0800 LockDemo[55316:1412919] 剩餘冰棍兒數= 0, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:40.709617+0800 LockDemo[55316:1412917] 冰棍兒賣光光  Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:41.714002+0800 LockDemo[55316:1412919] 冰棍兒賣光光  Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
複製程式碼
  • NSLock 類還增加了 tryLocklockBeforeDate: 方法
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
複製程式碼

tryLock 試圖獲取一個鎖,但是如果鎖不可用的時候,它不會阻塞執行緒,相反,它只是返回NO。 lockBeforeDate: 方法試圖獲取一個鎖,但是如果鎖沒有在規定的時間內被獲得,它會讓執行緒從阻塞狀態變為非阻塞狀態(或者返回NO)。

NSRecursiveLock 遞迴鎖

有時候“加鎖程式碼”中存在遞迴呼叫,遞迴開始前加鎖,遞迴呼叫開始後會重複執行此方法以至於反覆執行加鎖程式碼最終造成死鎖。

- (void)recursiveLockTest {
    //建立鎖
    _lock = [[NSLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^TestMethod)(int);
        TestMethod = ^(int value)
        {
            [_lock lock];
            if (value > 0)
            {
                [NSThread sleepForTimeInterval:1];
                value--;
                TestMethod(value);
            }
            [_lock unlock];
        };
        TestMethod(5);
        NSLog(@"結束");
    });
}
複製程式碼

我們發現 "結束" 永遠不會被列印出來,這個時候可以使用遞迴鎖來解決。使用遞迴鎖可以在一個執行緒中反覆獲取鎖而不造成死鎖,這個過程中會記錄獲取鎖和釋放鎖的次數,只有最後兩者平衡鎖才被最終釋放。

- (void)recursiveLockTest {
    //建立鎖
    _recursiveLock = [[NSRecursiveLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^TestMethod)(int);
        TestMethod = ^(int value)
        {
            [_recursiveLock lock];
            if (value > 0)
            {
                [NSThread sleepForTimeInterval:1];
                value--;
                TestMethod(value);
            }
            [_recursiveLock unlock];
        };
        TestMethod(5);
        NSLog(@"結束");
    });
}
複製程式碼

此時 "結束" 5秒後會被列印出來。

NSConditionLock 條件鎖

NSCoditionLock 做多執行緒之間的任務等待呼叫,而且是執行緒安全的。

- (void)conditionLockTest {
    
    NSInteger HAS_DATA = 1;
    NSInteger NO_DATA = 0;
    
    _conditionLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
    NSMutableArray *products = [NSMutableArray array];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            [_conditionLock lockWhenCondition:NO_DATA];
            [products addObject:[[NSObject alloc] init]];
            NSLog(@"生產");
            [_conditionLock unlockWithCondition:HAS_DATA];
            sleep(5);
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (1) {
            NSLog(@"等待");
            [_conditionLock lockWhenCondition:HAS_DATA];
            [products removeObjectAtIndex:0];
            NSLog(@"售賣");
            [_conditionLock unlockWithCondition:NO_DATA];
        }
    });
}
複製程式碼

NSConditionLock 也跟其它的鎖一樣,是需要 lockunlock 對應的,只是 lock , lockWhenCondition:unlockunlockWithCondition: 是可以隨意組合的,當然這是與需求相關的。

POSIX(pthread_mutex)

  • C語言定義下多執行緒加鎖方式。 pthread_mutex 和 dispatch_semaphore_t 很像,但是完全不同。pthread_mutex 是Unix/Linux平臺上提供的一套條件互斥鎖的API。
  • 新建一個簡單的 pthread_mutex 互斥鎖,引入標頭檔案 #import <pthread.h> 宣告並初始化一個 pthread_mutex_t 的結構。使用 pthread_mutex_lockpthread_mutex_unlock 函式。呼叫 pthread_mutex_destroy 來釋放該鎖的資料結構。

使用: #import <pthread.h>

- (void)pthreadTest {
    __block pthread_mutex_t theLock;
    pthread_mutex_init(&theLock, NULL);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        pthread_mutex_lock(&theLock);
        NSLog(@"第一個執行緒同步操作開始");
        sleep(3);
        NSLog(@"第一個執行緒同步操作結束");
        pthread_mutex_unlock(&theLock);
        
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        pthread_mutex_lock(&theLock);
        NSLog(@"第二個執行緒同步操作");
        pthread_mutex_unlock(&theLock);
        
    });
}
複製程式碼

執行結果:

2017-11-29 17:51:11.901064+0800 LockDemo[56729:1466788] 第一個執行緒同步操作開始
2017-11-29 17:51:14.904834+0800 LockDemo[56729:1466788] 第一個執行緒同步操作結束
2017-11-29 17:51:14.905195+0800 LockDemo[56729:1466789] 第二個執行緒同步操作
複製程式碼
  • pthread_mutex 還可以建立條件鎖,提供了和 NSCondition 一樣的條件控制,初始化互斥鎖同時使用 pthread_cond_init 來初始化條件資料結構
    // 初始化
    int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
    // 等待(會阻塞)
    int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
    // 定時等待
    int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
    // 喚醒
    int pthread_cond_signal (pthread_cond_t *cond);
    // 廣播喚醒
    int pthread_cond_broadcast (pthread_cond_t *cond);
    // 銷燬
    int pthread_cond_destroy (pthread_cond_t *cond);
複製程式碼

pthread_mutex 還提供了很多函式,有一套完整的API,包含 Pthreads 執行緒的建立控制等等,非常底層,可以手動處理執行緒的各個狀態的轉換即管理生命週期,甚至可以實現一套自己的多執行緒,感興趣的可以繼續深入瞭解。

dispatch_semaphore_t

dispatch_semaphore_t GCD中訊號量,也可以解決資源搶佔問題,支援訊號通知和訊號等待。每當傳送一個訊號通知,則訊號量 +1;每當傳送一個等待訊號時訊號量 -1,;如果訊號量為 0 則訊號會處於等待狀態,直到訊號量大於 0 開始執行。

api註釋:

/*! 
 * @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)semaphoreTest {
    // 建立訊號量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    //執行緒1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任務1");
        sleep(10);
        dispatch_semaphore_signal(semaphore);
    });
    
    //執行緒2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任務2");
        dispatch_semaphore_signal(semaphore);
    });
}
複製程式碼

執行結果:

2017-11-30 14:38:11.943521+0800 LockDemo[91493:2075379] 任務1
2017-11-30 14:38:21.946222+0800 LockDemo[91493:2075380] 任務2
複製程式碼

OSSpinLock 自旋鎖

使用: #import <libkern/OSAtomic.h>

__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    NSLog(@"第一個執行緒同步操作開始");
    sleep(3);
    NSLog(@"第一個執行緒同步操作結束");
    OSSpinLockUnlock(&theLock);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    sleep(1);
    NSLog(@"第二個執行緒同步操作");
    OSSpinLockUnlock(&theLock);
});
複製程式碼

執行結果:

2017-11-30 15:12:31.701180+0800 LockDemo[92422:2104479] 第一個執行緒同步操作開始
2017-11-30 15:12:39.705473+0800 LockDemo[92422:2104479] 第一個執行緒同步操作結束
2017-11-30 15:12:39.705820+0800 LockDemo[92422:2104478] 第二個執行緒同步操作開始
複製程式碼

OSSpinLock 自旋鎖,效能最高的鎖。它的缺點是當等待時會消耗大量 CPU 資源,不太適用於較長時間的任務。 YY大神在部落格 不再安全的 OSSpinLock 中說明了OSSpinLock已經不再安全,暫不建議使用。

iOS 10 之後,蘋果給出瞭解決方案,就是用 os_unfair_lock 代替 OSSpinLock。

'OSSpinLockLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock_lock() from <os/lock.h> instead
複製程式碼

#import <os/lock.h>

    __block os_unfair_lock  lock = OS_UNFAIR_LOCK_INIT;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        os_unfair_lock_lock(&lock);
        NSLog(@"第一個執行緒同步操作開始");
        sleep(8);
        NSLog(@"第一個執行緒同步操作結束");
        os_unfair_lock_unlock(&lock);
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        os_unfair_lock_lock(&lock);
        NSLog(@"第二個執行緒同步操作開始");
        os_unfair_lock_unlock(&lock);
    });
複製程式碼

執行結果:

2017-11-30 15:12:31.701180+0800 LockDemo[92422:2104479] 第一個執行緒同步操作開始
2017-11-30 15:12:39.705473+0800 LockDemo[92422:2104479] 第一個執行緒同步操作結束
2017-11-30 15:12:39.705820+0800 LockDemo[92422:2104478] 第二個執行緒同步操作開始
複製程式碼

總結

  • @synchronized:適用執行緒不多,任務量不大的多執行緒加鎖
  • NSLock:效能不算差,但感覺用的人不多。
  • dispatch_semaphore_t:使用訊號來做加鎖,效能很高和 OSSpinLock 差不多。
  • NSConditionLock:多執行緒處理不同任務的通訊建議時用, 只加鎖的話效能很低。
  • NSRecursiveLock:效能不錯,使用場景限制於遞迴。
  • POSIX(pthread_mutex):C語言的底層api,複雜的多執行緒處理建議使用,也可以封裝自己的多執行緒。
  • OSSpinLock:效能非常高,可惜不安全了,使用 os_unfair_lock 來代替。

相關文章