在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

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

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

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

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

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

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

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

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

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

線程

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

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

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

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

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

    // 開(kāi)始4個(gè)尋找最小值和最大值的線程
    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 對(duì) pthread 的一個(gè)封裝。通過(guò)封裝,在 Cocoa 環(huán)境中,可以讓代碼看起來(lái)更加親切。例如,開(kāi)發(fā)者可以利用 NSThread 的一個(gè)子類來(lái)定義一個(gè)線程,在這個(gè)子類的中封裝需要在后臺(tái)線程運(yùn)行的代碼。針對(duì)上面的那個(gè)例子,我們可以定義一個(gè)這樣的 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

要想啟動(dòng)一個(gè)新的線程,需要?jiǎng)?chuàng)建一個(gè)線程對(duì)象,然后調(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)在,我們可以通過(guò)檢測(cè)到線程的 isFinished 屬性來(lái)檢測(cè)新生成的線程是否已經(jīng)結(jié)束,并獲取結(jié)果。我們將這個(gè)練習(xí)留給感興趣的讀者,這主要是因?yàn)椴徽撌褂?pthread 還是 NSThread 來(lái)直接對(duì)線程操作,都是相對(duì)糟糕的編程體驗(yàn),這種方式并不適合我們以寫(xiě)出良好代碼為目標(biāo)的編碼精神。

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

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

Grand Central Dispatch

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

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

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

GCD 公開(kāi)有 5 個(gè)不同的隊(duì)列:運(yùn)行在主線程中的 main queue,3 個(gè)不同優(yōu)先級(jí)的后臺(tái)隊(duì)列,以及一個(gè)優(yōu)先級(jí)更低的后臺(tái)隊(duì)列(用于 I/O)。 另外,開(kāi)發(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)先級(jí)的若干個(gè)隊(duì)列乍聽(tīng)起來(lái)非常直接,不過(guò),我們強(qiáng)烈建議,在絕大多數(shù)情況下使用默認(rèn)的優(yōu)先級(jí)隊(duì)列就可以了。如果執(zhí)行的任務(wù)需要訪問(wèn)一些共享的資源,那么在不同優(yōu)先級(jí)的隊(duì)列中調(diào)度這些任務(wù)很快就會(huì)造成不可預(yù)期的行為。這樣可能會(huì)引起程序的完全掛起,因?yàn)榈蛢?yōu)先級(jí)的任務(wù)阻塞了高優(yōu)先級(jí)任務(wù),使它不能被執(zhí)行。更多相關(guān)內(nèi)容,在本文的優(yōu)先級(jí)反轉(zhuǎn)部分中會(huì)有介紹。

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

Operation Queues

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Run Loops

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

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

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

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

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

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

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

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

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

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

資源共享

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

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

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

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

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

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

互斥鎖

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

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

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

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

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

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

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

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

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

死鎖

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

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

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

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

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

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

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

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

資源饑餓(Starvation)

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

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

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

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

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

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

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

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

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

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

總結(jié)

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

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

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