iOS 多線程之線程安全

級別: ★★☆☆☆
標籤:「iOS」「多線程」「線程安全」
做者: dac_1033
審校: QiShare團隊php


1、線程安全問題

在單線程的情形下,任務依次串行執行是不存在線程安全問題的。在單線程的情形下,若是多線程都是訪問共享資源而不去修改共享資源也能夠保證線程安全,好比:設置只讀屬性的全局變量。線程不安全是因爲多線程訪問形成的,是因爲多線程訪問和修改共享資源而引發不可預測的結果。而線程鎖能夠有效的解決線程安全問題,大體過程以下圖:html

無線程鎖

加線程鎖

iOS 多線程開發中爲保證線程安全而經常使用的幾種鎖:NSLockdispatch_semaphoreNSConditionNSRecursiveLockNSConditionLock@synchronized,這幾種鎖各有優勢,適用於不一樣的場景,下面咱們就來依次介紹一下。git

2、iOS中的鎖

1. NSLock

NSLock 是OC層封裝底層線程操做來實現的一種鎖,繼承NSLocking協議,在此咱們不討論各類鎖的實現細節,由於基本用不到。NSLock使用很是簡單:github

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 提供的,使用信號量來控制併發線程的數量(可同時進入並執行加鎖代碼塊的線程的數量),相關的三個函數:bash

// 建立信號量
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技術交流羣》。

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

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

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

相關文章
相關標籤/搜索