iOS多執行緒整理

飛花蝶舞劍發表於2018-11-09

iOS多執行緒整理


知識點梳理

1.執行緒程式的區別:

> 程式:應用程式的例項
> 執行緒:任務排程的基本單元

2.佇列種類:

序列佇列、併發佇列、主佇列(有經過特殊處理的序列佇列)、全域性佇列(屬於併發佇列)

> 序列佇列:佇列中的任務按順序一個一個執行,任務的執行必須有先後順序
> 併發佇列:具有併發執行佇列中任務的能力
> 主佇列:繫結主執行緒,所有任務都在主執行緒中執行
> 全域性佇列:系統提供的併發佇列

序列並行的區別:

序列:表示在某個時刻只有一個任務在執行
並行:表示在某個時刻有多個任務在執行

3.併發與並行的區別:

併發 Concurrency [kən`kʌrənsɪ]:可以同時接受多個任務,使多個任務得到處理的特性

1.真實的情況。比如:一個程式猿可以攬10個需求同時去做。一個程式猿在做需求期間可抽空學習或接私活。一個工廠可以接10個訂單同時去生產。單核CPU可同時處理多個應用程式。

2.比如併發佇列。併發佇列能夠處理多個任務,使多個任務不用彼此等待同時得到處理。(擴充套件:併發佇列如何實現併發特性?通過開闢多個子執行緒去處理這多個任務,以此來實現併發特性)

3.比如單核CPU實現併發(擴充套件:單核CPU如何實現併發?通過時間片輪轉排程

並行 parallel [ˈpærəˌlɛl]:某個時刻多個任務能夠同時執行的能力

1.真實的情況。比如:人可以讓兩隻手一起握拳(人可並行握拳),一個打了10個孔的水管可以同時澆10盆花(打孔水管可並行澆花),等等…

2.比如多核CPU的平行計算,同一時刻CPU的每個核心可以單獨執行指令,上面說到的單核CPU是沒有這個能力的。

其他理解:

1.系統中有多個任務同時存在可稱之為“併發”,系統內有多個任務同時執行可稱之為“並行”

2.並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔發生

舉例:工廠加工糖果

不具有併發特性的工廠,無並行能力:工廠每次只能接一個訂單,多的訂單往後排,一個做完再做下一個;只有一臺機器生產糖果

具有併發特性的工廠,無並行能力:工廠可以一次性接多個訂單;只有一臺機器交替生產這多個訂單的糖果

不具有併發特性的工廠,具有並行能力:工廠每次只能接一個訂單,多的訂單往後排,一個做完再做下一個;有多臺機器一起生產這一個訂單的糖果

具有併發特性的工廠,具有並行能力:工廠可以一次性接多個訂單;有多臺機器一起生產這多個訂單的糖果

舉例:CPU執行任務

單核cpu非併發執行任務:單核CPU一次處理一個完整任務

單核cpu併發執行任務:單核CPU交替處理多個任務,每次只處理某個任務的一部分

多核cpu非併發執行任務:多核CPU一次處理一個任務,將任務拆分成多個子任務,多個核心同時單獨的執行這些子任務

多核cpu併發執行任務:多核CPU一次處理多個任務,將任務拆分,多個核心同時單獨的執行這些子任務

問題:

既然序列和並行是反義詞,為什麼都說併發佇列,而不說並行佇列:計算機硬體和系統可能並非能真正的並行執行任務。比如單核cpu,也可以實現併發,但是不具有並行能力。

4.操作:

> 同步:synchronize[ˈsɪŋkrənəs],同步任務需要使當前任務等待
> 非同步:asynchronous[e`sɪŋkrənəs],非同步任務無需使當前任務等待

同步非同步的理解:

我們寫的的程式碼其實是被包裹在一個任務中的,這個任務在佇列中排隊,然後輪到它時就在佇列繫結的執行緒中執行。

如下,整塊程式碼也都是被包裹在一個任務中,這個任務在主佇列排隊,然後最後輪到它時放到主執行緒執行。

// 任務1最開始....
...
// 以下的程式碼也只是任務1中的一個片段
// 主執行緒環境中
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 此處大括號包裹的整個是任務2
    // do something in task2
});
...
// 任務1最末尾...

等同於:

// 任務1最開始....
...
// 以下的程式碼也只是任務中的一個片段
// 主執行緒環境中
// do something in task1
...
// 任務1最末尾...

等價設定:

“當前執行緒執行的任務” <=> “任務1”
“需要執行的任務”:<=> “任務2”

1.同步:任務2與任務1同步,任務1要等待任務2執行完畢後才能繼續執行(擴充套件猜測:由於是同步,任務1要等待任務2,所以此時開新執行緒執行任務2和不開新執行緒執行任務2,從期望的結果來看沒什麼區別,則直接在當前執行緒中執行任務2即可)

2.非同步:任務2與任務1非同步,任務1不用等待任務2完成就可繼續執行

總結:同步非同步是針對多執行緒程式碼和當前所在環境之間的關係,用來控制“當前執行緒執行的任務”是否要等待“需要執行的任務”,與佇列無關,與佇列中的其他任務無關。

5.iOS中的多執行緒規則

情況 是否新開執行緒 與當前執行程式碼所屬任務的關係
序列佇列同步 當前執行緒執行 當前任務需等待
序列佇列非同步 新開執行緒執行(每個任務都在同一個執行緒執行) 當前任務無需等待
併發佇列同步 當前執行緒執行 當前任務需等待
併發佇列非同步 新開執行緒執行 當前任務無需等待

6.擴充套件知識:執行棧

1.常被用於存放子程式的返回地址

2.在呼叫任何子程式時,主程式都必須暫存子程式執行完畢後應該返回到的地址

3.如果被呼叫的子程式還要呼叫其他的子程式,其自身的返回地址就必須存入執行棧,在其自身執行完畢後再行取回

4.在遞迴程式中,每一層次遞迴都必須在執行棧上增加一條地址,因此如果程式出現無限遞迴(或僅僅是過多的遞迴層次),執行棧就會產生棧溢位。

比如:

void main() {
    int i = 0
    aMethod()
    bMethod()
}
void aMethod {
}
void bMethod {
    cMethod()
}
void cMethod {
}

堆疊過程:

null
main
main - aMethod
main
main - bMethod
main - bMethod - cMethod
main - bMethod
main
null

一些問題的理解

問題一:主執行緒環境中,在主佇列上執行同步任務,為什麼會死鎖

1.假設:假設當前執行的程式碼是包含在任務1中,在主佇列上執行的同步任務為任務2。

// 任務1
// 主執行緒環境中
dispatch_sync(dispatch_get_main_queue(), ^{
    // 任務2
    // do something in task2
});

2.同步角度思考:由於是是同步任務,所以任務1此時需要等待任務2執行,任務2執行完畢後任務1才能繼續執行下去。

3.佇列角度思考:任務2會被加到主佇列的隊尾,由於序列佇列的特性,任務必須一個一個執行。因此任務2需要等待佇列中其他任務(包括任務1)都執行完之後才會輪到它去執行。

4.結果:所以出現了任務2等待任務1,任務1等待任務2的情況,導致死鎖。
此外如果序列佇列繫結執行緒a,那麼線上程a環境中,在該序列佇列上執行同步任務,也會導致死鎖。原因同上。

問題二:主執行緒環境中,為什麼在新建立的序列佇列中執行同步任務就不會死鎖

1.假設:假設當前執行的程式碼是包含在任務1中,在序列佇列上執行的同步任務為任務2。

// 任務1
// 主執行緒環境中
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 任務2
    // do something in task2
});

2.同步角度思考:由於是是同步任務,所以任務1此時需要等待任務2執行,任務2執行完畢後任務1才能繼續執行下去。

3.佇列角度思考:任務2會被加到序列佇列zcp的隊尾,任務2只跟佇列zcp中的其他任務有先後順序關係,跟其他佇列上的任務無關,也就是說任務2跟主佇列中的其他任務無關,所以任務2不會等待任務1

4.結果:任務1等待任務2,任務2不用等待任務1,任務2執行完畢後,然後繼續執行任務1。

API介紹

詳細內容可參考:iOS多執行緒-歸納與總結

1.NSThread

管理多執行緒困難,推薦使用NSOperation和GCD。

應用場景:

> 1.使用[NSThread currentThread]獲取當前執行緒
> 2.使用[NSThread mainThread]獲取主執行緒

2.NSOperation

GCD的封裝,程式碼風格更OC。

特點:

1.可以控制暫停、恢復、停止。suspended、cancel、cancelAllOperations

2.可以控制任務的優先順序。threadPriority和queuePriority

3.可以設定依賴關係。addDependency和removeDependency

4.可以控制併發個數。maxConcurrentOperationCount

5.NSOperation有兩個封裝的便利子類NSBlockOperation、NSInvocationOperation,他們都使用了併發佇列

佇列的種類:

主佇列 [NSOperationQueue mainQueue],是序列佇列

非主佇列 [NSOperationQueue new],是併發佇列

NSOperation的執行過程:

當operation加入到queue中時,會在相關執行緒中執行operation的start方法,main方法在start方法中呼叫。

執行緒判定:

根據queue來決定在哪個執行緒中執行start方法。

[NSOperationQueue mainQueue]:在主執行緒中執行

[NSOperationQueue currentQueue]:在當前執行緒中執行

[NSOperationQueue new]:新開執行緒執行,該佇列為併發佇列

start方法和main方法的執行順序:

start方法內部做了一些有關安全的邏輯判斷,判斷結束後執行main。

因此如果自己寫了一個類繼承自NSOperation,重寫start方法時要注意,main方法會在[super start]中執行,如果不呼叫[super start]則main方法不執行,另外要注意[super start]與前後程式碼的執行順序。

應用場景:

可以參考AFNetworking2.x版本中的AFURLConnectionOperation類和AFHTTPRequestOperation

3.GCD

1.佇列與操作

佇列與操作

// 序列佇列
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL)
// 併發佇列
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_CONCURRENT)
// 主佇列
dispatch_queue_t queue = dispatch_get_main_queue()
// 全域性佇列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// 同步操作
dispatch_sync(queue, ^{
})
// 非同步操作
dispatch_async(queue, ^{
})

其他功能

// 暫停佇列
dispatch_suspend(dispatch_object_t object);
// 恢復佇列
dispatch_resume(dispatch_object_t object);

2.其他內容

dispatch_after

// 延遲5秒執行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});

dispatch_once

確保程式執行過程中只被執行一次,且執行緒安全,常用於單例。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});

任務組

用來處理多個任務都完成後再執行的動作

// 佇列,可以根據情況使用合適的queue
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_CONCURRENT);
// 建立任務組
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    // 任務1
});
dispatch_group_async(group, queue, ^{
    // 任務2
});
dispatch_group_async(group, queue, ^{
    // 任務3
});
dispatch_group_notify(group, queue, ^{
    // 任務1、任務2、任務3都執行完畢之後才會執行這裡
});


// 系統管理佇列組:
dispatch_group_async(group, queue, ^{
    // do something
});
// 等價於
// 手動管理佇列組:
dispatch_group_enter(group);
dispatch_async(queue, ^{
    // do something
    dispatch_group_leave(group);
});

dispatch_semaphore

// 建立訊號量
dispatch_semaphore_create
// 訊號量-1
dispatch_semaphore_wait
// 訊號量+1
dispatch_semaphore_signal
// YYCache中的YYDiskCache類中
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)

dispatch_barrier

參見:iOS多執行緒程式設計總結

dispatch_barrier_async:
dispatch_async(queue, block1_for_reading)  
dispatch_async(queue, block2_for_reading)

dispatch_barrier_async(queue, block_for_writing)

dispatch_async(queue, block3_for_reading)  
dispatch_async(queue, block4_for_reading)  

/*
dispatch_barrier_async會把並行佇列的執行週期分為這三個過程:

首先等目前追加到並行佇列中所有任務都執行完成
開始執行dispatch_barrier_async中的任務,這時候即使向並行佇列提交任務,也不會執行
dispatch_barrier_async中的任務執行完成後,並行佇列恢復正常。

這樣一來,使用並行佇列和dispatc_barrier_async方法,就可以高效的進行資料和檔案讀寫了。
*/

4.其他問題

多執行緒與runloop的關係:

每個執行緒都有一個runloop,主執行緒預設開啟,子執行緒預設休眠。
一般來講,一個執行緒一次只能執行一個任務,執行完畢後執行緒就會退出,開啟runloop可以讓執行緒能隨時處理事件但並不退出

多執行緒不安全的情況:

__block int a = 0;
for (int i = 0; i < 100; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"%d", a++);
    });
}

// 結果並不為100

原因:

a++這句程式碼其實相當於a = a+1,由於程式碼是在併發佇列中非同步執行的,所以相當於有100個a = a+1同時執行。

a++程式碼的執行過程如下:

> 1.取到a的值
> 2.計算a+1的值
> 3.將計算結果賦值給a

正常情況:

第1次:執行a++,取到a為0,計算a+1結果為1,將1賦值給a。

異常情況:

第1次:執行a++,取到a為0,
第10次:執行a++,取到a為0,計算a+1結果為1,將1賦值給a
第56次:執行a++,取到a為1,計算a+1結果為2,將2賦值給a
第27次:執行a++,取到a為2,計算a+1結果為3,將3賦值給a
第1次:計算a+1結果為1,將1賦值給a

以上是一種異常情況的假設,實際的執行情況會更復雜。在第一次取到a為0時,其他執行緒已經跑了很多句a++的程式碼使a變成了3,這個時候才開始計算第一次的a+1,a又變成了1。導致前面幾次的計算都沒意義了。
引用時間片輪轉排程中的一段話:

在自己的程式執行時不是獨一無二的,我們看似很順暢的工作,其實是由一個個的執行片段構成的,我們眼中相鄰的兩條語句甚至同一個語句中兩個不同的運算子之間,都有可能插入其他執行緒或程式的動作。


使用案例

1.處理耗時任務

本地持久化:如果在主執行緒中儲存資料,資料量比較大時會阻塞主執行緒造成頁面卡頓。需要新開執行緒在後臺處理。另外還有使用dispatch_barrier_async和CoreData的案例。

耗時程式碼處理:如果使用多次數的迴圈語句,或者是使用非常耗時的api時,會影響到主執行緒導致卡頓。可以新開執行緒在後臺處理,然後如果有需要重新整理UI則在主執行緒中同步。

2.網路請求等待

介面請求:介面請求受網路環境影響,是不可能在主執行緒請求並等待的。需要新開執行緒非同步請求。如使用NSURLSession的dataTaskWithURL:方法(或NSURLConnection的sendAsynchronousRequest:方法)非同步請求;如AFNetworking中非同步請求程式碼。

載入網路資源:載入網路中的大圖或下載檔案會很耗時,需要在後臺執行緒載入。如在子執行緒中使用NSData的dataWithContentsOfURL:下載檔案;如SDWebImage的非同步下載。

3.其他情況

任務組:常會遇到某個邏輯判斷需要兩個介面中的資料,比如當獲取業務線和列表資料之後才渲染頁面。就可以用任務組。

延遲呼叫:比如一些動畫的實現需要延時。


後續

iOS中的鎖


參考文章

iOS多執行緒-歸納與總結

iOS多執行緒程式設計總結

GCD 深入理解:第一部分


相關文章