iOS 多執行緒之執行緒安全

QiShare發表於2019-01-24

級別: ★★☆☆☆
標籤:「iOS」「多執行緒」「執行緒安全」
作者: dac_1033
審校: QiShare團隊


一、執行緒安全問題

在單執行緒的情形下,任務依次序列執行是不存線上程安全問題的。在單執行緒的情形下,如果多執行緒都是訪問共享資源而不去修改共享資源也可以保證執行緒安全,比如:設定只讀屬性的全域性變數。執行緒不安全是由於多執行緒訪問造成的,是由於多執行緒訪問和修改共享資源而引起不可預測的結果。而執行緒鎖可以有效的解決執行緒安全問題,大致過程如下圖:

無執行緒鎖

加執行緒鎖

iOS 多執行緒開發中為保證執行緒安全而常用的幾種鎖:NSLockdispatch_semaphoreNSConditionNSRecursiveLockNSConditionLock@synchronized,這幾種鎖各有優點,適用於不同的場景,下面我們就來依次介紹一下。

二、iOS中的鎖

1. NSLock

NSLock 是OC層封裝底層執行緒操作來實現的一種鎖,繼承NSLocking協議,在此我們不討論各種鎖的實現細節,因為基本用不到。NSLock使用非常簡單:

NSLock *lock = [NSLock alloc] init];

// 加鎖
[lock lock];

/*
* 被加鎖的程式碼區間
*/

// 解鎖
[lock Unlock];
複製程式碼

我們以車站購票為例子,多個視窗同時售票,每個視窗有人迴圈購票:

// 定義NSLock變數
@property (nonatomic, strong) NSLock *lock;
// 例項化
_lock = [[NSLock alloc] init];

/*******************************************************************************/

// 呼叫測試方法
dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSLock];
        });
    }
}

/*******************************************************************************/

// 測試方法
- (void)testNSLock {
    
    while (1) {
        [_lock lock];
        if (_ticketCount > 0) {
            _ticketCount --;
            NSLog(@"--->> %@已購票1張,剩餘%ld張", [NSThread currentThread], (long)_ticketCount);
        }
        else {
            [_lock unlock];
            return;
        }
        [_lock unlock];
        sleep(0.2);
    }
}
複製程式碼
2. dispatch_semaphore

dispatch_semaphore 是 GCD 提供的,使用訊號量來控制併發執行緒的數量(可同時進入並執行加鎖程式碼塊的執行緒的數量),相關的三個函式:

// 建立訊號量
dispatch_semaphore_create(long value); 

//等待訊號
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

傳送訊號
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
複製程式碼
//! 定義訊號量semaphore
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
//! 例項化
_semaphore = dispatch_semaphore_create(1);

/*******************************************************************************/

// 呼叫測試方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    for (NSInteger i=0; i<2; i++) {
        dispatch_async(queue, ^{
            [self testDispatchSemaphore:i];
        });
    }
}

/*******************************************************************************/

// 測試方法
- (void)testDispatchSemaphore:(NSInteger)num {
    
    while (1) {
        // 引數1為訊號量;引數2為超時時間;ret為返回值
        //dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        long ret = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.21*NSEC_PER_SEC)));
        if (ret == 0) {
            if (_ticketCount > 0) {
                NSLog(@"%d 視窗 賣了第%d張票", (int)num, (int)_ticketCount);
                _ticketCount --;
            }
            else {
                dispatch_semaphore_signal(_semaphore);
                NSLog(@"%d 賣光了", (int)num);
                break;
            }
            [NSThread sleepForTimeInterval:0.2];
            dispatch_semaphore_signal(_semaphore);
        }
        else {
            NSLog(@"%d %@", (int)num, @"超時了");
        }
        
        [NSThread sleepForTimeInterval:0.2];
    }
}
複製程式碼

當第一各引數semaphore取值為1時,dispatch_semaphore_wait(semaphore, timeout)與dispatch_semaphore_signal(signal)成對出現,所達到的效果就跟NSLock中的lock和unlock是一樣的。區別在於當semaphore取值為n時,則可以有n個執行緒同時訪問被保護的臨界區,即可以控制多個執行緒併發。第二個引數為dispatch_time_t型別,如果直接輸入一個非dispatch_time_t的值會導致dispatch_semaphore_wait方法偶爾返回非0值。

3. NSCondition

NSCondition 常用於生產者-消費者模式,它繼承於NSLocking協議,同樣有lock和unlock方法。條件變數有點像訊號量,提供了執行緒阻塞與訊號機制,因此可以用來阻塞某個執行緒,並等待資料就緒,再喚醒執行緒。

NSCondition *lock = [[NSCondition alloc] init];

//執行緒A
[lock lock];

[lock wait]; // 執行緒被掛起

[lock unlock];

//執行緒2
sleep(1);//以保證讓執行緒2的程式碼後執行

[lock lock];

[lock signal]; // 喚醒執行緒1

[lock unlock];
複製程式碼

我們執行了兩次for迴圈,起了兩批新執行緒,一批來add資料,另一批來remove資料。其中add資料方法加鎖,remove資料方法也加了鎖:

// 定義變數
@property (nonatomic, strong) NSCondition *condition;
// 例項化
_condition = [[NSCondition alloc] init];

/*******************************************************************************/

// 呼叫測試方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionAdd];
        });
    }

    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionRemove];
        });
    }
}

/*******************************************************************************/

// 測試方法
- (void)testNSConditionAdd {
    
    [_condition lock];
    
    // 生產資料
    NSObject *object = [NSObject new];
    [_ticketsArr addObject:object];
    NSLog(@"--->>%@ add", [NSThread currentThread]);
    [_condition signal];
    
    [_condition unlock];
}

- (void)testNSConditionRemove {
    
    [_condition lock];
    
    // 消費資料
    if (!_ticketsArr.count) {
        NSLog(@"--->> wait");
        [_condition wait];
    }
    [_ticketsArr removeObjectAtIndex:0];
    NSLog(@"--->>%@ remove", [NSThread currentThread]);
    
    [_condition unlock];
}
複製程式碼
4. NSConditionLock

NSConditionLock 為條件鎖,lockWhenCondition:方法是當condition引數與初始化時候的 condition 相等時才可加鎖。而unlockWithCondition:方法並不是當 Condition 符合條件時才解鎖,而是解鎖之後,修改 Condition 的值。NSConditionLock 藉助 NSCondition 來實現,它的本質就是一個生產者-消費者模型。“條件被滿足”可以理解為生產者提供了新的內容NSConditionLock 的內部持有一個 NSCondition 物件,以及 _condition_value 屬性,在初始化時就會對這個屬性進行賦值:

// 設定條件
#define CONDITION_NO_DATA   100
#define CONDITION_HAS_DATA  101

/*******************************************************************************/

// 初始化條件鎖物件
@property (nonatomic, strong) NSConditionLock *conditionLock;
// 例項化
_conditionLock = [[NSConditionLock alloc] initWithCondition:CONDITION_NO_DATA];

/*******************************************************************************/

// 呼叫測試方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionLockAdd];
        });
    }

    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionLockRemove];
        });
    }
}

/*******************************************************************************/

// 測試方法
- (void)testNSConditionLockAdd {
    
    // 滿足CONDITION_NO_DATA時,加鎖
    [_conditionLock lockWhenCondition:CONDITION_NO_DATA];
    
    // 生產資料
    NSObject *object = [NSObject new];
    [_ticketsArr addObject:object];
    NSLog(@"---->>%@ add", [NSThread currentThread]);
    [_condition signal];
    
    // 有資料,解鎖並設定條件
    [_conditionLock unlockWithCondition:CONDITION_HAS_DATA];
}

- (void)testNSConditionLockRemove {
    
    // 有資料時,加鎖
    [_conditionLock lockWhenCondition:CONDITION_HAS_DATA];
    
    // 消費資料
    if (!_ticketsArr.count) {
        NSLog(@"---->> wait");
        [_condition wait];
    }
    [_ticketsArr removeObjectAtIndex:0];
    NSLog(@"---->>%@ remove", [NSThread currentThread]);
    
    //3. 沒有資料,解鎖並設定條件
    [_conditionLock unlockWithCondition:CONDITION_NO_DATA];
}
複製程式碼
5. NSRecursiveLock

顧名思義,NSRecursiveLock定義的是一個遞迴鎖,這個鎖可以被同一執行緒多次請求,而不會引起死鎖。這主要是用在迴圈或遞迴操作中。NSRecursiveLock在識別到遞迴時,只加1次鎖,在遞迴返回時也只解鎖1次。

// 初始化鎖物件
@property (nonatomic, strong) NSRecursiveLock *recursiveLock;
_recursiveLock = [[NSRecursiveLock alloc] init];

/*******************************************************************************/

// 加鎖的遞迴方法
- (void)testNSRecursiveLock:(NSInteger)tag {
    
    [_recursiveLock lock];
    
    if (tag > 0) {
        
        [self testNSRecursiveLock:tag - 1];
        NSLog(@"--->> %ld", (long)tag);
    }
    
    [_recursiveLock unlock];
}
複製程式碼
6. @synchronized

@synchronized是一個 OC 層面的鎖,非常簡單易用。引數需要傳一個 OC 物件,它實際上是把這個物件當做鎖的唯一標識。使用時直接將加鎖的程式碼區間放入花括號中即可,但是它的缺點也顯而易見,雖然易用,但是沒有之上介紹幾個鎖的複雜功能

- (void)testSynchronized {
    
    @synchronized (self) {
        
        if (_ticketCount > 0) {
            
            _ticketCount --;
            NSLog(@"--->> %@已購票1張,剩餘%ld張", [NSThread currentThread], (long)_ticketCount);
        }
    }
}

複製程式碼

原子操作 原子操作是指不可打斷的操作,也就是說執行緒在執行操作過程中,不會被作業系統掛起,而是一定會執行完。如文章開頭出圖中17+1 = 18這個動作,在整個運算過程中,就屬於一個原子操作。
變數屬性Property中的原子定義 一般我們定義一個變數 @property (nonatomic, strong) NSMutableArray *ticketsArr; nonatomic:非原子屬性,不會為setter方法加鎖,適合記憶體小的移動裝置; atomic:原子屬性,預設為setter方法加鎖(預設就是atomic),執行緒安全。
PS: 在iOS開發過程中,一般都將屬性宣告為nonatomic,儘量避免多執行緒搶奪同一資源,儘量將加鎖等資源搶奪業務交給伺服器。

本文參考了以下文章:

工程原始碼GitHub地址


小編微信:可加並拉入《QiShare技術交流群》。

iOS 多執行緒之執行緒安全

關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

推薦文章:
iOS 多執行緒之GCD
iOS 多執行緒之NSOperation
iOS 多執行緒之NSThread
iOS Winding Rules 纏繞規則
iOS 簽名機制
iOS 掃描二維碼/條形碼
奇舞週刊

推薦活動:
360粉絲團:136個新年福袋,你確定不來參加嗎?

相關文章