鍍金池/ 教程/ iOS/ 并發(fā)編程:API 及挑戰(zhàn)
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動畫
常見的后臺實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

并發(fā)編程:API 及挑戰(zhàn)

并發(fā)所描述的概念就是同時運(yùn)行多個任務(wù)。這些任務(wù)可能是以在單核 CPU 上分時(時間共享)的形式同時運(yùn)行,也可能是在多核 CPU 上以真正的并行方式來運(yùn)行。

OS X 和 iOS 提供了幾種不同的 API 來支持并發(fā)編程。每一個 API 都具有不同的功能和使用限制,這使它們適合不同的任務(wù)。同時,這些 API 處在不同的抽象層級上。我們有可能用其進(jìn)行非常深入底層的操作,但是這也意味著背負(fù)起將任務(wù)進(jìn)行良好處理的巨大責(zé)任。

實(shí)際上,并發(fā)編程是一個很有挑戰(zhàn)的主題,它有許多錯綜復(fù)雜的問題和陷阱。當(dāng)開發(fā)者在使用類似 Grand Central Dispatch(GCD)或 NSOperationQueue 的 API 時,很容易遺忘這些問題和陷阱。本文首先對 OS X 和 iOS 中不同的并發(fā)編程 API 進(jìn)行一些介紹,然后再深入了解并發(fā)編程中獨(dú)立于與你所使用的特定 API 的一些內(nèi)在挑戰(zhàn)。

OS X 和 iOS 中的并發(fā)編程

蘋果的移動和桌面操作系統(tǒng)中提供了相同的并發(fā)編程API。 本文會介紹 pthreadNSThread 、GCD 、NSOperationQueue,以及 NSRunLoop。實(shí)際上把 run loop 也列在其中是有點(diǎn)奇怪,因?yàn)樗⒉荒軐?shí)現(xiàn)真正的并行,不過因?yàn)樗c并發(fā)編程有莫大的關(guān)系,因此值得我們進(jìn)行一些深入了解。

由于高層 API 是基于底層 API 構(gòu)建的,所以我們首先將從底層的 API 開始介紹,然后逐步擴(kuò)展到高層 API。不過在具體編程中,選擇 API 的順序剛好相反:因?yàn)榇蠖鄶?shù)情況下,選擇高層的 API 不僅可以完成底層 API 能完成的任務(wù),而且能夠讓并發(fā)模型變得簡單。

如果你對我們?yōu)楹螆?jiān)持推薦使用高抽象層級以及簡單的并行代碼有所疑問的話,那么你可以看看這篇文章的第二部分并發(fā)編程中面臨的挑戰(zhàn),以及 Peter Steinberger 寫的關(guān)于線程安全的文章。

線程

線程(thread)是組成進(jìn)程的子單元,操作系統(tǒng)的調(diào)度器可以對線程進(jìn)行單獨(dú)的調(diào)度。實(shí)際上,所有的并發(fā)編程 API 都是構(gòu)建于線程之上的 —— 包括 GCD 和操作隊(duì)列(operation queues)。

多線程可以在單核 CPU 上同時(或者至少看作同時)運(yùn)行。操作系統(tǒng)將小的時間片分配給每一個線程,這樣就能夠讓用戶感覺到有多個任務(wù)在同時進(jìn)行。如果 CPU 是多核的,那么線程就可以真正的以并發(fā)方式被執(zhí)行,從而減少了完成某項(xiàng)操作所需要的總時間。

你可以使用 Instruments 中的 CPU strategy view 來得知你的代碼或者你在使用的框架代碼是如何在多核 CPU 中調(diào)度執(zhí)行的。

需要重點(diǎn)關(guān)注的是,你無法控制你的代碼在什么地方以及什么時候被調(diào)度,以及無法控制執(zhí)行多長時間后將被暫停,以便輪換執(zhí)行別的任務(wù)。這種線程調(diào)度是非常強(qiáng)大的一種技術(shù),但是也非常復(fù)雜,我們稍后研究。

先把線程調(diào)度的復(fù)雜情況放一邊,開發(fā)者可以使用 POSIX 線程 API,或者 Objective-C 中提供的對該 API 的封裝 NSThread,來創(chuàng)建自己的線程。下面這個小示例利用 pthread 來在一百萬個數(shù)字中查找最小值和最大值。其中并發(fā)執(zhí)行了 4 個線程。從該示例復(fù)雜的代碼中,應(yīng)該可以看出為什么你不會希望直接使用 pthread 。

#import <pthread.h>

struct threadInfo {
    uint32_t * inputValues;
    size_t count;
};

struct threadResult {
    uint32_t min;
    uint32_t max;
};

void * findMinAndMax(void *arg)
{
    struct threadInfo const * const info = (struct threadInfo *) arg;
    uint32_t min = UINT32_MAX;
    uint32_t max = 0;
    for (size_t i = 0; i < info->count; ++i) {
        uint32_t v = info->inputValues[i];
        min = MIN(min, v);
        max = MAX(max, v);
    }
    free(arg);
    struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
    result->min = min;
    result->max = max;
    return result;
}

int main(int argc, const char * argv[])
{
    size_t const count = 1000000;
    uint32_t inputValues[count];

    // 使用隨機(jī)數(shù)字填充 inputValues
    for (size_t i = 0; i < count; ++i) {
        inputValues[i] = arc4random();
    }

    // 開始4個尋找最小值和最大值的線程
    size_t const threadCount = 4;
    pthread_t tid[threadCount];
    for (size_t i = 0; i < threadCount; ++i) {
        struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));
        size_t offset = (count / threadCount) * i;
        info->inputValues = inputValues + offset;
        info->count = MIN(count - offset, count / threadCount);
        int err = pthread_create(tid + i, NULL, &findMinAndMax, info);
        NSCAssert(err == 0, @"pthread_create() failed: %d", err);
    }
    // 等待線程退出
    struct threadResult * results[threadCount];
    for (size_t i = 0; i < threadCount; ++i) {
        int err = pthread_join(tid[i], (void **) &(results[i]));
        NSCAssert(err == 0, @"pthread_join() failed: %d", err);
    }
    // 尋找 min 和 max
    uint32_t min = UINT32_MAX;
    uint32_t max = 0;
    for (size_t i = 0; i < threadCount; ++i) {
        min = MIN(min, results[i]->min);
        max = MAX(max, results[i]->max);
        free(results[i]);
        results[i] = NULL;
    }

    NSLog(@"min = %u", min);
    NSLog(@"max = %u", max);
    return 0;
}

NSThread 是 Objective-C 對 pthread 的一個封裝。通過封裝,在 Cocoa 環(huán)境中,可以讓代碼看起來更加親切。例如,開發(fā)者可以利用 NSThread 的一個子類來定義一個線程,在這個子類的中封裝需要在后臺線程運(yùn)行的代碼。針對上面的那個例子,我們可以定義一個這樣的 NSThread 子類:

@interface FindMinMaxThread : NSThread
@property (nonatomic) NSUInteger min;
@property (nonatomic) NSUInteger max;
- (instancetype)initWithNumbers:(NSArray *)numbers;
@end

@implementation FindMinMaxThread {
    NSArray *_numbers;
}

- (instancetype)initWithNumbers:(NSArray *)numbers
{
    self = [super init];
    if (self) {
        _numbers = numbers;
    }
    return self;
}

- (void)main
{
    NSUInteger min;
    NSUInteger max;
    // 進(jìn)行相關(guān)數(shù)據(jù)的處理
    self.min = min;
    self.max = max;
}
@end

要想啟動一個新的線程,需要創(chuàng)建一個線程對象,然后調(diào)用它的 start 方法:

NSMutableSet *threads = [NSMutableSet set];
NSUInteger numberCount = self.numbers.count;
NSUInteger threadCount = 4;
for (NSUInteger i = 0; i < threadCount; i++) {
    NSUInteger offset = (count / threadCount) * i;
    NSUInteger count = MIN(numberCount - offset, numberCount / threadCount);
    NSRange range = NSMakeRange(offset, count);
    NSArray *subset = [self.numbers subarrayWithRange:range];
    FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset];
    [threads addObject:thread];
    [thread start];
}

現(xiàn)在,我們可以通過檢測到線程的 isFinished 屬性來檢測新生成的線程是否已經(jīng)結(jié)束,并獲取結(jié)果。我們將這個練習(xí)留給感興趣的讀者,這主要是因?yàn)椴徽撌褂?pthread 還是 NSThread 來直接對線程操作,都是相對糟糕的編程體驗(yàn),這種方式并不適合我們以寫出良好代碼為目標(biāo)的編碼精神。

直接使用線程可能會引發(fā)的一個問題是,如果你的代碼和所基于的框架代碼都創(chuàng)建自己的線程時,那么活動的線程數(shù)量有可能以指數(shù)級增長。這在大型工程中是一個常見問題。例如,在 8 核 CPU 中,你創(chuàng)建了 8 個線程來完全發(fā)揮 CPU 性能。然而在這些線程中你的代碼所調(diào)用的框架代碼也做了同樣事情(因?yàn)樗⒉恢滥阋呀?jīng)創(chuàng)建的這些線程),這樣會很快產(chǎn)生成成百上千的線程。代碼的每個部分自身都沒有問題,然而最后卻還是導(dǎo)致了問題。使用線程并不是沒有代價的,每個線程都會消耗一些內(nèi)存和內(nèi)核資源。

接下來,我們將介紹兩個基于隊(duì)列的并發(fā)編程 API :GCD 和 operation queue 。它們通過集中管理一個被大家協(xié)同使用的線程池,來解決上面遇到的問題。

Grand Central Dispatch

為了讓開發(fā)者更加容易的使用設(shè)備上的多核CPU,蘋果在 OS X 10.6 和 iOS 4 中引入了 Grand Central Dispatch(GCD)。在下一篇關(guān)于底層并發(fā) API 的文章中,我們將更深入地介紹 GCD。

通過 GCD,開發(fā)者不用再直接跟線程打交道了,只需要向隊(duì)列中添加代碼塊即可,GCD 在后端管理著一個線程池。GCD 不僅決定著你的代碼塊將在哪個線程被執(zhí)行,它還根據(jù)可用的系統(tǒng)資源對這些線程進(jìn)行管理。這樣可以將開發(fā)者從線程管理的工作中解放出來,通過集中的管理線程,來緩解大量線程被創(chuàng)建的問題。

GCD 帶來的另一個重要改變是,作為開發(fā)者可以將工作考慮為一個隊(duì)列,而不是一堆線程,這種并行的抽象模型更容易掌握和使用。

GCD 公開有 5 個不同的隊(duì)列:運(yùn)行在主線程中的 main queue,3 個不同優(yōu)先級的后臺隊(duì)列,以及一個優(yōu)先級更低的后臺隊(duì)列(用于 I/O)。 另外,開發(fā)者可以創(chuàng)建自定義隊(duì)列:串行或者并行隊(duì)列。自定義隊(duì)列非常強(qiáng)大,在自定義隊(duì)列中被調(diào)度的所有 block 最終都將被放入到系統(tǒng)的全局隊(duì)列中和線程池中。

http://wiki.jikexueyuan.com/project/objc/images/2-1.png" alt="GCD queues" />

使用不同優(yōu)先級的若干個隊(duì)列乍聽起來非常直接,不過,我們強(qiáng)烈建議,在絕大多數(shù)情況下使用默認(rèn)的優(yōu)先級隊(duì)列就可以了。如果執(zhí)行的任務(wù)需要訪問一些共享的資源,那么在不同優(yōu)先級的隊(duì)列中調(diào)度這些任務(wù)很快就會造成不可預(yù)期的行為。這樣可能會引起程序的完全掛起,因?yàn)榈蛢?yōu)先級的任務(wù)阻塞了高優(yōu)先級任務(wù),使它不能被執(zhí)行。更多相關(guān)內(nèi)容,在本文的優(yōu)先級反轉(zhuǎn)部分中會有介紹。

雖然 GCD 是一個低層級的 C API ,但是它使用起來非常的直接。不過這也容易使開發(fā)者忘記并發(fā)編程中的許多注意事項(xiàng)和陷阱。讀者可以閱讀本文后面的并發(fā)編程中面臨的挑戰(zhàn),這樣可以注意到一些潛在的問題。本期的另外一篇優(yōu)秀文章:底層并發(fā) API 中,包含了很多深入的解釋和一些有價值的提示。

Operation Queues

操作隊(duì)列(operation queue)是由 GCD 提供的一個隊(duì)列模型的 Cocoa 抽象。GCD 提供了更加底層的控制,而操作隊(duì)列則在 GCD 之上實(shí)現(xiàn)了一些方便的功能,這些功能對于 app 的開發(fā)者來說通常是最好最安全的選擇。

NSOperationQueue 有兩種不同類型的隊(duì)列:主隊(duì)列和自定義隊(duì)列。主隊(duì)列運(yùn)行在主線程之上,而自定義隊(duì)列在后臺執(zhí)行。在兩種類型中,這些隊(duì)列所處理的任務(wù)都使用 NSOperation 的子類來表述。

你可以通過重寫 main 或者 start 方法 來定義自己的 operations 。前一種方法非常簡單,開發(fā)者不需要管理一些狀態(tài)屬性(例如 isExecutingisFinished),當(dāng) main 方法返回的時候,這個 operation 就結(jié)束了。這種方式使用起來非常簡單,但是靈活性相對重寫 start 來說要少一些。

@implementation YourOperation
    - (void)main
    {
        // 進(jìn)行處理 ...
    }
@end

如果你希望擁有更多的控制權(quán),以及在一個操作中可以執(zhí)行異步任務(wù),那么就重寫 start 方法:

@implementation YourOperation
    - (void)start
    {
        self.isExecuting = YES;
        self.isFinished = NO;
        // 開始處理,在結(jié)束時應(yīng)該調(diào)用 finished ...
    }

    - (void)finished
    {
        self.isExecuting = NO;
        self.isFinished = YES;
    }
@end

注意:這種情況下,你必須手動管理操作的狀態(tài)。 為了讓操作隊(duì)列能夠捕獲到操作的改變,需要將狀態(tài)的屬性以配合 KVO 的方式進(jìn)行實(shí)現(xiàn)。如果你不使用它們默認(rèn)的 setter 來進(jìn)行設(shè)置的話,你就需要在合適的時候發(fā)送合適的 KVO 消息。

為了能使用操作隊(duì)列所提供的取消功能,你需要在長時間操作中時不時地檢查 isCancelled 屬性:

- (void)main
{
    while (notDone && !self.isCancelled) {
        // 進(jìn)行處理
    }
}

當(dāng)你定義好 operation 類之后,就可以很容易的將一個 operation 添加到隊(duì)列中:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
YourOperation *operation = [[YourOperation alloc] init];
[queue  addOperation:operation];

另外,你也可以將 block 添加到操作隊(duì)列中。這有時候會非常的方便,比如你希望在主隊(duì)列中調(diào)度一個一次性任務(wù):

[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    // 代碼...
}];

雖然通過這種的方式在隊(duì)列中添加操作會非常方便,但是定義你自己的 NSOperation 子類會在調(diào)試時很有幫助。如果你重寫 operation 的description 方法,就可以很容易的標(biāo)示出在某個隊(duì)列中當(dāng)前被調(diào)度的所有操作 。

除了提供基本的調(diào)度操作或 block 外,操作隊(duì)列還提供了在 GCD 中不太容易處理好的特性的功能。例如,你可以通過 maxConcurrentOperationCount 屬性來控制一個特定隊(duì)列中可以有多少個操作參與并發(fā)執(zhí)行。將其設(shè)置為 1 的話,你將得到一個串行隊(duì)列,這在以隔離為目的的時候會很有用。

另外還有一個方便的功能就是根據(jù)隊(duì)列中 operation 的優(yōu)先級對其進(jìn)行排序,這不同于 GCD 的隊(duì)列優(yōu)先級,它只影響當(dāng)前隊(duì)列中所有被調(diào)度的 operation 的執(zhí)行先后。如果你需要進(jìn)一步在除了 5 個標(biāo)準(zhǔn)的優(yōu)先級以外對 operation 的執(zhí)行順序進(jìn)行控制的話,還可以在 operation 之間指定依賴關(guān)系,如下:

[intermediateOperation addDependency:operation1];
[intermediateOperation addDependency:operation2];
[finishedOperation addDependency:intermediateOperation];

這些簡單的代碼可以確保 operation1operation2intermediateOperation 之前執(zhí)行,當(dāng)然,也會在 finishOperation 之前被執(zhí)行。對于需要明確的執(zhí)行順序時,操作依賴是非常強(qiáng)大的一個機(jī)制。它可以讓你創(chuàng)建一些操作組,并確保這些操作組在依賴它們的操作被執(zhí)行之前執(zhí)行,或者在并發(fā)隊(duì)列中以串行的方式執(zhí)行操作。

從本質(zhì)上來看,操作隊(duì)列的性能比 GCD 要低那么一點(diǎn),不過,大多數(shù)情況下這點(diǎn)負(fù)面影響可以忽略不計(jì),操作隊(duì)列是并發(fā)編程的首選工具。

Run Loops

實(shí)際上,Run loop并不像 GCD 或者操作隊(duì)列那樣是一種并發(fā)機(jī)制,因?yàn)樗⒉荒懿⑿袌?zhí)行任務(wù)。不過在主 dispatch/operation 隊(duì)列中, run loop 將直接配合任務(wù)的執(zhí)行,它提供了一種異步執(zhí)行代碼的機(jī)制。

Run loop 比起操作隊(duì)列或者 GCD 來說容易使用得多,因?yàn)橥ㄟ^ run loop ,你不必處理并發(fā)中的復(fù)雜情況,就能異步地執(zhí)行任務(wù)。

一個 run loop 總是綁定到某個特定的線程中。main run loop 是與主線程相關(guān)的,在每一個 Cocoa 和 CocoaTouch 程序中,這個 main run loop 都扮演了一個核心角色,它負(fù)責(zé)處理 UI 事件、計(jì)時器,以及其它內(nèi)核相關(guān)事件。無論你什么時候設(shè)置計(jì)時器、使用 NSURLConnection 或者調(diào)用 performSelector:withObject:afterDelay:,其實(shí)背后都是 run loop 在處理這些異步任務(wù)。

無論何時你使用 run loop 來執(zhí)行一個方法的時候,都需要記住一點(diǎn):run loop 可以運(yùn)行在不同的模式中,每種模式都定義了一組事件,供 run loop 做出響應(yīng)。這在對應(yīng) main run loop 中暫時性的將某個任務(wù)優(yōu)先執(zhí)行這種任務(wù)上是一種聰明的做法。

關(guān)于這點(diǎn),在 iOS 中非常典型的一個示例就是滾動。在進(jìn)行滾動時,run loop 并不是運(yùn)行在默認(rèn)模式中的,因此, run loop 此時并不會響應(yīng)比如滾動前設(shè)置的計(jì)時器。一旦滾動停止了,run loop 會回到默認(rèn)模式,并執(zhí)行添加到隊(duì)列中的相關(guān)事件。如果在滾動時,希望計(jì)時器能被觸發(fā),需要將其設(shè)為 NSRunLoopCommonModes 的模式,并添加到 run loop 中。

主線程一般來說都已經(jīng)配置好了 main run loop。然而其他線程默認(rèn)情況下都沒有設(shè)置 run loop。你也可以自行為其他線程設(shè)置 run loop ,但是一般來說我們很少需要這么做。大多數(shù)時間使用 main run loop 會容易得多。如果你需要處理一些很重的工作,但是又不想在主線程里做,你仍然可以在你的代碼在 main run loop 中被調(diào)用后將工作分配給其他隊(duì)列。Chris 在他關(guān)于常見的后臺實(shí)踐的文章里闡述了一些關(guān)于這種模式的很好的例子。

如果你真需要在別的線程中添加一個 run loop ,那么不要忘記在 run loop 中至少添加一個 input source 。如果 run loop 中沒有設(shè)置好的 input source,那么每次運(yùn)行這個 run loop ,它都會立即退出。

并發(fā)編程中面臨的挑戰(zhàn)

使用并發(fā)編程會帶來許多陷阱。只要一旦你做的事情超過了最基本的情況,對于并發(fā)執(zhí)行的多任務(wù)之間的相互影響的不同狀態(tài)的監(jiān)視就會變得異常困難。 問題往往發(fā)生在一些不確定性(不可預(yù)見性)的地方,這使得在調(diào)試相關(guān)并發(fā)代碼時更加困難。

關(guān)于并發(fā)編程的不可預(yù)見性有一個非常有名的例子:在1995年, NASA (美國宇航局)發(fā)送了開拓者號火星探測器,但是當(dāng)探測器成功著陸在我們紅色的鄰居星球后不久,任務(wù)嘎然而止,火星探測器莫名其妙的不停重啟,在計(jì)算機(jī)領(lǐng)域內(nèi),遇到的這種現(xiàn)象被定為為優(yōu)先級反轉(zhuǎn),也就是說低優(yōu)先級的線程一直阻塞著高優(yōu)先級的線程。稍后我們會看到關(guān)于這個問題的更多細(xì)節(jié)。在這里我們想說明的是,即使擁有豐富的資源和大量優(yōu)秀工程師的智慧,并發(fā)也還是會在不少情況下反咬你你一口。

資源共享

并發(fā)編程中許多問題的根源就是在多線程中訪問共享資源。資源可以是一個屬性、一個對象,通用的內(nèi)存、網(wǎng)絡(luò)設(shè)備或者一個文件等等。在多線程中任何一個共享的資源都可能是一個潛在的沖突點(diǎn),你必須精心設(shè)計(jì)以防止這種沖突的發(fā)生。

為了演示這類問題,我們舉一個關(guān)于資源的簡單示例:比如僅僅用一個整型值來做計(jì)數(shù)器。在程序運(yùn)行過程中,我們有兩個并行線程 A 和 B,這兩個線程都嘗試著同時增加計(jì)數(shù)器的值。問題來了,你通過 C 語言或 Objective-C 寫的代碼大多數(shù)情況下對于 CPU 來說不會僅僅是一條機(jī)器指令。要想增加計(jì)數(shù)器的值,當(dāng)前的必須被從內(nèi)存中讀出,然后增加計(jì)數(shù)器的值,最后還需要將這個增加后的值寫回內(nèi)存中。

我們可以試著想一下,如果兩個線程同時做上面涉及到的操作,會發(fā)生怎樣的偶然。例如,線程 A 和 B 都從內(nèi)存中讀取出了計(jì)數(shù)器的值,假設(shè)為 17 ,然后線程A將計(jì)數(shù)器的值加1,并將結(jié)果 18 寫回到內(nèi)存中。同時,線程B也將計(jì)數(shù)器的值加 1 ,并將結(jié)果 18 寫回到內(nèi)存中。實(shí)際上,此時計(jì)數(shù)器的值已經(jīng)被破壞掉了,因?yàn)橛?jì)數(shù)器的值 17 被加 1 了兩次,而它的值卻是 18。

http://wiki.jikexueyuan.com/project/objc/images/2-2.png" alt="競態(tài)條件" />

這個問題被叫做競態(tài)條件,在多線程里面訪問一個共享的資源,如果沒有一種機(jī)制來確保在線程 A 結(jié)束訪問一個共享資源之前,線程 B 就不會開始訪問該共享資源的話,資源競爭的問題就總是會發(fā)生。如果你所寫入內(nèi)存的并不是一個簡單的整數(shù),而是一個更復(fù)雜的數(shù)據(jù)結(jié)構(gòu),可能會發(fā)生這樣的現(xiàn)象:當(dāng)?shù)谝粋€線程正在寫入這個數(shù)據(jù)結(jié)構(gòu)時,第二個線程卻嘗試讀取這個數(shù)據(jù)結(jié)構(gòu),那么獲取到的數(shù)據(jù)可能是新舊參半或者沒有初始化。為了防止出現(xiàn)這樣的問題,多線程需要一種互斥的機(jī)制來訪問共享資源。

在實(shí)際的開發(fā)中,情況甚至要比上面介紹的更加復(fù)雜,因?yàn)楝F(xiàn)代 CPU 為了優(yōu)化目的,往往會改變向內(nèi)存讀寫數(shù)據(jù)的順序(亂序執(zhí)行)。

互斥鎖

互斥訪問的意思就是同一時刻,只允許一個線程訪問某個特定資源。為了保證這一點(diǎn),每個希望訪問共享資源的線程,首先需要獲得一個共享資源的互斥鎖,一旦某個線程對資源完成了操作,就釋放掉這個互斥鎖,這樣別的線程就有機(jī)會訪問該共享資源了。

http://wiki.jikexueyuan.com/project/objc/images/2-3.png" alt="互斥鎖" />

除了確?;コ庠L問,還需要解決代碼無序執(zhí)行所帶來的問題。如果不能確保 CPU 訪問內(nèi)存的順序跟編程時的代碼指令一樣,那么僅僅依靠互斥訪問是不夠的。為了解決由 CPU 的優(yōu)化策略引起的副作用,還需要引入內(nèi)存屏障。通過設(shè)置內(nèi)存屏障,來確保沒有無序執(zhí)行的指令能跨過屏障而執(zhí)行。

當(dāng)然,互斥鎖自身的實(shí)現(xiàn)是需要沒有競爭條件的。這實(shí)際上是非常重要的一個保證,并且需要在現(xiàn)代 CPU 上使用特殊的指令。更多關(guān)于原子操作(atomic operation)的信息,請閱讀 Daniel 寫的文章:底層并發(fā)技術(shù)。

從語言層面來說,在 Objective-C 中將屬性以 atomic 的形式來聲明,就能支持互斥鎖了。事實(shí)上在默認(rèn)情況下,屬性就是 atomic 的。將一個屬性聲明為 atomic 表示每次訪問該屬性都會進(jìn)行隱式的加鎖和解鎖操作。雖然最把穩(wěn)的做法就是將所有的屬性都聲明為 atomic,但是加解鎖這也會付出一定的代價。

在資源上的加鎖會引發(fā)一定的性能代價。獲取鎖和釋放鎖的操作本身也需要沒有競態(tài)條件,這在多核系統(tǒng)中是很重要的。另外,在獲取鎖的時候,線程有時候需要等待,因?yàn)榭赡芷渌木€程已經(jīng)獲取過資源的鎖了。這種情況下,線程會進(jìn)入休眠狀態(tài)。當(dāng)其它線程釋放掉相關(guān)資源的鎖時,休眠的線程會得到通知。所有這些相關(guān)操作都是非常昂貴且復(fù)雜的。

鎖也有不同的類型。當(dāng)沒有競爭時,有些鎖在沒有鎖競爭的情況下性能很好,但是在有鎖的競爭情況下,性能就會大打折扣。另外一些鎖則在基本層面上就比較耗費(fèi)資源,但是在競爭情況下,性能的惡化會沒那么厲害。(鎖的競爭是這樣產(chǎn)生的:當(dāng)一個或者多個線程嘗試獲取一個已經(jīng)被別的線程獲取過了的鎖)。

在這里有一個東西需要進(jìn)行權(quán)衡:獲取和釋放鎖所是要帶來開銷的,因此你需要確保你不會頻繁地進(jìn)入和退出臨界區(qū)段(比如獲取和釋放鎖)。同時,如果你獲取鎖之后要執(zhí)行一大段代碼,這將帶來鎖競爭的風(fēng)險:其它線程可能必須等待獲取資源鎖而無法工作。這并不是一項(xiàng)容易解決的任務(wù)。

我們經(jīng)常能看到本來計(jì)劃并行運(yùn)行的代碼,但實(shí)際上由于共享資源中配置了相關(guān)的鎖,所以同一時間只有一個線程是處于激活狀態(tài)的。對于你的代碼會如何在多核上運(yùn)行的預(yù)測往往十分重要,你可以使用 Instrument 的 CPU strategy view 來檢查是否有效的利用了 CPU 的可用核數(shù),進(jìn)而得出更好的想法,以此來優(yōu)化代碼。

死鎖

互斥鎖解決了競態(tài)條件的問題,但很不幸同時這也引入了一些其他問題,其中一個就是死鎖。當(dāng)多個線程在相互等待著對方的結(jié)束時,就會發(fā)生死鎖,這時程序可能會被卡住。

http://wiki.jikexueyuan.com/project/objc/images/2-4.png" alt="死鎖" />

看看下面的代碼,它交換兩個變量的值:

void swap(A, B)
{
    lock(lockA);
    lock(lockB);
    int a = A;
    int b = B;
    A = b;
    B = a;
    unlock(lockB);
    unlock(lockA);
}

大多數(shù)時候,這能夠正常運(yùn)行。但是當(dāng)兩個線程使用相反的值來同時調(diào)用上面這個方法時:

swap(X, Y); // 線程 1
swap(Y, X); // 線程 2

此時程序可能會由于死鎖而被終止。線程 1 獲得了 X 的一個鎖,線程 2 獲得了 Y 的一個鎖。 接著它們會同時等待另外一把鎖,但是永遠(yuǎn)都不會獲得。

再說一次,你在線程之間共享的資源越多,你使用的鎖也就越多,同時程序被死鎖的概率也會變大。這也是為什么我們需要盡量減少線程間資源共享,并確保共享的資源盡量簡單的原因之一。建議閱讀一下底層并發(fā)編程 API 中的全部使用異步分發(fā)一節(jié)。

資源饑餓(Starvation)

當(dāng)你認(rèn)為已經(jīng)足夠了解并發(fā)編程面臨的問題時,又出現(xiàn)了一個新的問題。鎖定的共享資源會引起讀寫問題。大多數(shù)情況下,限制資源一次只能有一個線程進(jìn)行讀取訪問其實(shí)是非常浪費(fèi)的。因此,在資源上沒有寫入鎖的時候,持有一個讀取鎖是被允許的。這種情況下,如果一個持有讀取鎖的線程在等待獲取寫入鎖的時候,其他希望讀取資源的線程則因?yàn)闊o法獲得這個讀取鎖而導(dǎo)致資源饑餓的發(fā)生。

為了解決這個問題,我們需要使用一個比簡單的讀/寫鎖更聰明的方法,例如給定一個 writer preference,或者使用 read-copy-update 算法。Daniel 在底層并發(fā)編程 API 中有介紹了如何用 GCD 實(shí)現(xiàn)一個多讀取單寫入的模式,這樣就不會被寫入資源饑餓的問題困擾了。

優(yōu)先級反轉(zhuǎn)

本節(jié)開頭介紹了美國宇航局發(fā)射的開拓者號火星探測器在火星上遇到的并發(fā)問題?,F(xiàn)在我們就來看看為什么開拓者號幾近失敗,以及為什么有時候我們的程序也會遇到相同的問題,該死的優(yōu)先級反轉(zhuǎn)。

優(yōu)先級反轉(zhuǎn)是指程序在運(yùn)行時低優(yōu)先級的任務(wù)阻塞了高優(yōu)先級的任務(wù),有效的反轉(zhuǎn)了任務(wù)的優(yōu)先級。由于 GCD 提供了擁有不同優(yōu)先級的后臺隊(duì)列,甚至包括一個 I/O 隊(duì)列,所以我們最好了解一下優(yōu)先級反轉(zhuǎn)的可能性。

高優(yōu)先級和低優(yōu)先級的任務(wù)之間共享資源時,就可能發(fā)生優(yōu)先級反轉(zhuǎn)。當(dāng)?shù)蛢?yōu)先級的任務(wù)獲得了共享資源的鎖時,該任務(wù)應(yīng)該迅速完成,并釋放掉鎖,這樣高優(yōu)先級的任務(wù)就可以在沒有明顯延時的情況下繼續(xù)執(zhí)行。然而高優(yōu)先級任務(wù)會在低優(yōu)先級的任務(wù)持有鎖的期間被阻塞。如果這時候有一個中優(yōu)先級的任務(wù)(該任務(wù)不需要那個共享資源),那么它就有可能會搶占低優(yōu)先級任務(wù)而被執(zhí)行,因?yàn)榇藭r高優(yōu)先級任務(wù)是被阻塞的,所以中優(yōu)先級任務(wù)是目前所有可運(yùn)行任務(wù)中優(yōu)先級最高的。此時,中優(yōu)先級任務(wù)就會阻塞著低優(yōu)先級任務(wù),導(dǎo)致低優(yōu)先級任務(wù)不能釋放掉鎖,這也就會引起高優(yōu)先級任務(wù)一直在等待鎖的釋放。

http://wiki.jikexueyuan.com/project/objc/images/2-5.png" alt="優(yōu)先級反轉(zhuǎn)" />

在你的實(shí)際代碼中,可能不會像發(fā)生在火星的事情那樣戲劇性地不停重啟。遇到優(yōu)先級反轉(zhuǎn)時,一般沒那么嚴(yán)重。

解決這個問題的方法,通常就是不要使用不同的優(yōu)先級。通常最后你都會以讓高優(yōu)先級的代碼等待低優(yōu)先級的代碼來解決問題。當(dāng)你使用 GCD 時,總是使用默認(rèn)的優(yōu)先級隊(duì)列(直接使用,或者作為目標(biāo)隊(duì)列)。如果你使用不同的優(yōu)先級,很可能實(shí)際情況會讓事情變得更糟糕。

從中得到的教訓(xùn)是,使用不同優(yōu)先級的多個隊(duì)列聽起來雖然不錯,但畢竟是紙上談兵。它將讓本來就復(fù)雜的并行編程變得更加復(fù)雜和不可預(yù)見。如果你在編程中,遇到高優(yōu)先級的任務(wù)突然沒理由地卡住了,可能你會想起本文,以及那個美國宇航局的工程師也遇到過的被稱為優(yōu)先級反轉(zhuǎn)的問題。

總結(jié)

我們希望通過本文你能夠了解到并發(fā)編程帶來的復(fù)雜性和相關(guān)問題。并發(fā)編程中,無論是看起來多么簡單的 API ,它們所能產(chǎn)生的問題會變得非常的難以觀測,而且要想調(diào)試這類問題往往也都是非常困難的。

但另一方面,并發(fā)實(shí)際上是一個非常棒的工具。它充分利用了現(xiàn)代多核 CPU 的強(qiáng)大計(jì)算能力。在開發(fā)中,關(guān)鍵的一點(diǎn)就是盡量讓并發(fā)模型保持簡單,這樣可以限制所需要的鎖的數(shù)量。

我們建議采納的安全模式是這樣的:從主線程中提取出要使用到的數(shù)據(jù),并利用一個操作隊(duì)列在后臺處理相關(guān)的數(shù)據(jù),最后回到主隊(duì)列中來發(fā)送你在后臺隊(duì)列中得到的結(jié)果。使用這種方式,你不需要自己做任何鎖操作,這也就大大減少了犯錯誤的幾率。