iOS 多線程之線程安全

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


一蜜氨、線程安全問題

在單線程的情形下黔姜,任務(wù)依次串行執(zhí)行是不存在線程安全問題的墩剖。在單線程的情形下,如果多線程都是訪問共享資源而不去修改共享資源也可以保證線程安全,比如:設(shè)置只讀屬性的全局變量。線程不安全是由于多線程訪問造成的凤薛,是由于多線程訪問和修改共享資源而引起不可預(yù)測的結(jié)果。而線程鎖可以有效的解決線程安全問題诞仓,大致過程如下圖:

無線程鎖
加線程鎖

iOS 多線程開發(fā)中為保證線程安全而常用的幾種鎖:NSLock缤苫、dispatch_semaphoreNSCondition狂芋、NSRecursiveLock榨馁、NSConditionLock@synchronized帜矾,這幾種鎖各有優(yōu)點,適用于不同的場景屑柔,下面我們就來依次介紹一下屡萤。

二、iOS中的鎖

1. NSLock

NSLock 是OC層封裝底層線程操作來實現(xiàn)的一種鎖掸宛,繼承NSLocking協(xié)議死陆,在此我們不討論各種鎖的實現(xiàn)細節(jié),因為基本用不到。NSLock使用非常簡單:

NSLock *lock = [NSLock alloc] init];

// 加鎖
[lock lock];

/*
* 被加鎖的代碼區(qū)間
*/

// 解鎖
[lock Unlock];

我們以車站購票為例子措译,多個窗口同時售票别凤,每個窗口有人循環(huán)購票:

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

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

// 調(diào)用測試方法
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 提供的领虹,使用信號量來控制并發(fā)線程的數(shù)量(可同時進入并執(zhí)行加鎖代碼塊的線程的數(shù)量)规哪,相關(guān)的三個函數(shù):

// 創(chuàng)建信號量
dispatch_semaphore_create(long value); 

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

發(fā)送信號
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
//! 定義信號量semaphore
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
//! 實例化
_semaphore = dispatch_semaphore_create(1);

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

// 調(diào)用測試方法
- (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) {
        // 參數(shù)1為信號量;參數(shù)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];
    }
}

當?shù)谝桓鲄?shù)semaphore取值為1時诉稍,dispatch_semaphore_wait(semaphore, timeout)與dispatch_semaphore_signal(signal)成對出現(xiàn),所達到的效果就跟NSLock中的lock和unlock是一樣的最疆。區(qū)別在于當semaphore取值為n時杯巨,則可以有n個線程同時訪問被保護的臨界區(qū),即可以控制多個線程并發(fā)努酸。第二個參數(shù)為dispatch_time_t類型服爷,如果直接輸入一個非dispatch_time_t的值會導致dispatch_semaphore_wait方法偶爾返回非0值。

3. NSCondition

NSCondition 常用于生產(chǎn)者-消費者模式获诈,它繼承于NSLocking協(xié)議仍源,同樣有l(wèi)ock和unlock方法。條件變量有點像信號量烙荷,提供了線程阻塞與信號機制镜会,因此可以用來阻塞某個線程,并等待數(shù)據(jù)就緒终抽,再喚醒線程戳表。

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

//線程A
[lock lock];

[lock wait]; // 線程被掛起

[lock unlock];

//線程2
sleep(1);//以保證讓線程2的代碼后執(zhí)行

[lock lock];

[lock signal]; // 喚醒線程1

[lock unlock];

我們執(zhí)行了兩次for循環(huán),起了兩批新線程昼伴,一批來add數(shù)據(jù)匾旭,另一批來remove數(shù)據(jù)。其中add數(shù)據(jù)方法加鎖圃郊,remove數(shù)據(jù)方法也加了鎖:

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

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

// 調(diào)用測試方法
- (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];
    
    // 生產(chǎn)數(shù)據(jù)
    NSObject *object = [NSObject new];
    [_ticketsArr addObject:object];
    NSLog(@"--->>%@ add", [NSThread currentThread]);
    [_condition signal];
    
    [_condition unlock];
}

- (void)testNSConditionRemove {
    
    [_condition lock];
    
    // 消費數(shù)據(jù)
    if (!_ticketsArr.count) {
        NSLog(@"--->> wait");
        [_condition wait];
    }
    [_ticketsArr removeObjectAtIndex:0];
    NSLog(@"--->>%@ remove", [NSThread currentThread]);
    
    [_condition unlock];
}
4. NSConditionLock

NSConditionLock 為條件鎖价涝,lockWhenCondition:方法是當condition參數(shù)與初始化時候的 condition 相等時才可加鎖。而unlockWithCondition:方法并不是當 Condition 符合條件時才解鎖持舆,而是解鎖之后色瘩,修改 Condition 的值。NSConditionLock 借助 NSCondition 來實現(xiàn)逸寓,它的本質(zhì)就是一個生產(chǎn)者-消費者模型居兆。“條件被滿足”可以理解為生產(chǎn)者提供了新的內(nèi)容NSConditionLock 的內(nèi)部持有一個 NSCondition 對象竹伸,以及 _condition_value 屬性泥栖,在初始化時就會對這個屬性進行賦值:

// 設(shè)置條件
#define CONDITION_NO_DATA   100
#define CONDITION_HAS_DATA  101

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

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

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

// 調(diào)用測試方法
- (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];
    
    // 生產(chǎn)數(shù)據(jù)
    NSObject *object = [NSObject new];
    [_ticketsArr addObject:object];
    NSLog(@"---->>%@ add", [NSThread currentThread]);
    [_condition signal];
    
    // 有數(shù)據(jù),解鎖并設(shè)置條件
    [_conditionLock unlockWithCondition:CONDITION_HAS_DATA];
}

- (void)testNSConditionLockRemove {
    
    // 有數(shù)據(jù)時吧享,加鎖
    [_conditionLock lockWhenCondition:CONDITION_HAS_DATA];
    
    // 消費數(shù)據(jù)
    if (!_ticketsArr.count) {
        NSLog(@"---->> wait");
        [_condition wait];
    }
    [_ticketsArr removeObjectAtIndex:0];
    NSLog(@"---->>%@ remove", [NSThread currentThread]);
    
    //3. 沒有數(shù)據(jù)魏割,解鎖并設(shè)置條件
    [_conditionLock unlockWithCondition:CONDITION_NO_DATA];
}
5. NSRecursiveLock

顧名思義,NSRecursiveLock定義的是一個遞歸鎖钢颂,這個鎖可以被同一線程多次請求钞它,而不會引起死鎖。這主要是用在循環(huán)或遞歸操作中甸陌。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 層面的鎖耻卡,非常簡單易用。參數(shù)需要傳一個 OC 對象牲尺,它實際上是把這個對象當做鎖的唯一標識卵酪。使用時直接將加鎖的代碼區(qū)間放入花括號中即可,但是它的缺點也顯而易見谤碳,雖然易用溃卡,但是沒有之上介紹幾個鎖的復雜功能

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

原子操作
原子操作是指不可打斷的操作蜒简,也就是說線程在執(zhí)行操作過程中瘸羡,不會被操作系統(tǒng)掛起,而是一定會執(zhí)行完搓茬。如文章開頭出圖中17+1 = 18這個動作犹赖,在整個運算過程中,就屬于一個原子操作卷仑。
變量屬性Property中的原子定義
一般我們定義一個變量 @property (nonatomic, strong) NSMutableArray *ticketsArr;
nonatomic:非原子屬性峻村,不會為setter方法加鎖,適合內(nèi)存小的移動設(shè)備锡凝;
atomic:原子屬性粘昨,默認為setter方法加鎖(默認就是atomic),線程安全窜锯。

PS: 在iOS開發(fā)過程中张肾,一般都將屬性聲明為nonatomic,盡量避免多線程搶奪同一資源锚扎,盡量將加鎖等資源搶奪業(yè)務(wù)交給服務(wù)器捌浩。

本文參考了以下文章:

工程源碼GitHub地址


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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市助币,隨后出現(xiàn)的幾起案子浪听,更是在濱河造成了極大的恐慌,老刑警劉巖眉菱,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迹栓,死亡現(xiàn)場離奇詭異,居然都是意外死亡俭缓,警方通過查閱死者的電腦和手機克伊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來华坦,“玉大人愿吹,你說我怎么就攤上這事∠Ы悖” “怎么了犁跪?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長歹袁。 經(jīng)常有香客問我坷衍,道長,這世上最難降的妖魔是什么条舔? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮孟抗,結(jié)果婚禮上迁杨,老公的妹妹穿的比我還像新娘。我一直安慰自己夸浅,他們只是感情好仑最,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著警医,像睡著了一般预皇。 火紅的嫁衣襯著肌膚如雪潘悼。 梳的紋絲不亂的頭發(fā)上爬橡,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天治唤,我揣著相機與錄音,去河邊找鬼糙申。 笑死宾添,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的柜裸。 我是一名探鬼主播缕陕,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼疙挺!你這毒婦竟也來了扛邑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤衔统,失蹤者是張志新(化名)和其女友劉穎鹿榜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锦爵,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡舱殿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了险掀。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沪袭。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖樟氢,靈堂內(nèi)的尸體忽然破棺而出冈绊,到底是詐尸還是另有隱情,我是刑警寧澤埠啃,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布死宣,位于F島的核電站,受9級特大地震影響碴开,放射性物質(zhì)發(fā)生泄漏毅该。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一潦牛、第九天 我趴在偏房一處隱蔽的房頂上張望眶掌。 院中可真熱鬧,春花似錦巴碗、人聲如沸朴爬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽召噩。三九已至母赵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蚣常,已是汗流浹背市咽。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抵蚊,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓溯革,卻偏偏與公主長得像贞绳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子致稀,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 線程安全問題 在單線程的情形下冈闭,任務(wù)依次串行執(zhí)行是不存在線程安全問題的。在單線程的情形下抖单,如果多線程都是訪問共享資...
    大成小棧閱讀 316評論 0 0
  • 多線程的使用提升了程序的性能從而提升了用戶體驗萎攒,但是同時也有一定的風險,那就是多個線程同時修改某一個資源造成的資源...
    隨風流逝閱讀 414評論 0 1
  • 鎖是一種同步機制矛绘,用于多線程環(huán)境中對資源訪問的限制iOS中常見鎖的性能對比圖(摘自:ibireme): iOS鎖的...
    LiLS閱讀 1,516評論 0 6
  • 轉(zhuǎn)載自:http://www.reibang.com/p/938d68ed832c# 一耍休、前言 前段時間看了幾個...
    cafei閱讀 4,540評論 1 12
  • 幸福是一種喵喵的感受 快樂是一種汪汪的活著 . 你總是在我耳邊嘰嘰喳喳的叫著 讓我想起了咩咩年幼的美好生活 . 那...
    水搖絹閱讀 184評論 3 0