本文純屬個人讀書筆記。
本文主要分為以下模組:
一、知識點
二、執行緒的生命週期:新建 - 就緒 - 執行 - 阻塞 - 死亡
三、多執行緒的四種方案:Pthread、NSThread、GCD、NSOperation
四、執行緒安全問題
五、NSThread的使用
六、GCD的理解與使用
七、NSOperation的理解與使用
一、知識點:
- CPU時間片,每個一個獲得CPU任務只能執行一個時間片規定的時間。
- 執行緒就是一段程式碼以及執行時資料。
- 每個應用程式都是一個程式。
- 一個程式的所有任務都線上程中進行,每個程式都至少有一個執行緒(主執行緒)。
- 主執行緒:處理UI,所有更新UI的操作都必須在主執行緒上執行。不要把耗時操作放在主執行緒,會卡介面。
- 多執行緒:在同一時刻,一個CPU只能處理1條執行緒,但CPU可以在多條執行緒之間快速的切換,只要切換的足夠快,就造成了多執行緒一同執行的假象。
我們運用多執行緒的目的是:將耗時的操作放在後臺執行!
二、執行緒的生命週期:新建 - 就緒 - 執行 - 阻塞 - 死亡
1. 新建:例項化執行緒物件
2. 就緒:向執行緒物件傳送start訊息,執行緒物件被加入可排程執行緒池等待CPU排程
3. 執行:被CPU執行。在執行完成前,狀態可能會在就緒和執行之間來回切換。又CPU負責。
4. 阻塞:當滿足某個預定條件是,可以使用休眠或鎖,阻塞執行緒執行。
* sleepForTimeInterval (休眠指定時長),
* sleepUntiDate(休眠到指定日期),
* @synchronized(self):(互斥鎖)
5. 死亡:
* 正常死亡,執行緒執行完畢。
* 非正常死亡,當滿足某個條件後,線上程內部終止執行/在主執行緒終止執行緒物件。
6. 執行緒的exit和cancel:
* [thread exit]: 一旦強行終止執行緒,後續的所有程式碼都不會被執行。
* [thread cancel]: 預設情況(延遲取消),它就是給pthread設定取消標誌,
pthread執行緒在很多時候會檢視自己是否有取消請求。
複製程式碼
三、多執行緒的四種方案:Pthread、NSThread、GCD、NSOperation
1. Pthread: POSIX執行緒(POSIX threads),簡稱Pthreads,是執行緒的POSIX標準。
運用C語言,是一套通用的API,可跨平臺Unix/Linux/Windows。執行緒的生命週期由程式設計師管理。
2. NSThread:物件導向,可直接操作執行緒物件。執行緒的生命週期由程式設計師管理。
3. GCD:代替NSThread,可以充分利用裝置的多核,自動管理執行緒生命週期。
4. NSOperation:底層是GCD,比GCD多了一些方法,更加物件導向,自動管理執行緒生命週期。
複製程式碼
四、執行緒安全問題
當多個執行緒訪問同一塊資源時,很容易引發資料錯亂和資料安全問題。
複製程式碼
解決多執行緒安全問題方案:
1. 方法一:互斥鎖(同步鎖)
用於保護臨界區,確保同一時間只有一個執行緒訪問資料。
如果程式碼中只有一個地方需要加鎖,大多都使用self作為鎖物件,這樣可以避免單獨再建立一個鎖物件。
加了互斥做的程式碼,當新執行緒訪問時,如果發現其他執行緒正在執行鎖定的程式碼,新執行緒就會進入休眠。
@synchronized(鎖物件){
//TO DO
}
複製程式碼
2. 方法一:自旋鎖
與互斥量類似,它不是通過休眠使程式阻塞,而是在獲取鎖之前一直處於忙等(自旋)阻塞狀態。
用在以下情況:鎖持有的時間短,而且執行緒並不希望在重新排程上花太多的成本。"原地打轉"。
自旋鎖與互斥鎖的區別:執行緒在申請自旋鎖的時候,執行緒不會被掛起,而是處於忙等的狀態。
加了自旋鎖,當新執行緒訪問程式碼時,如果發現有其他執行緒正在鎖定程式碼,新執行緒會用死迴圈的方式,一直等待鎖定的程式碼執行完成。相當於不停嘗試執行程式碼,比較消耗效能。 屬性修飾atomic本身就有一把自旋鎖。
屬性修飾atomic和nonatomic
atomic(預設):原子屬性(執行緒安全),保證同一時間只有一個執行緒能夠寫入(但是同一個時間多個執行緒都可以取值),atomic 本身就有一把鎖(自旋鎖) ,需要消耗大量的資源。
nonatomic:非原子屬性(非執行緒安全),同一時間可以有很多執行緒讀和寫,多執行緒情況下資料可能會有問題!不過效率更高,一般情況下使用nonatomic。
五、NSThread的使用
1. NSThread建立執行緒
- init方式
- detachNewThreadSelector建立好之後自動啟動
- performSelectorInBackground建立好之後也是直接啟動
複製程式碼
- (void) createNSThread{
NSString *threadName1 = @"NSThread1";
NSString *threadName2 = @"NSThread2";
NSString *threadName3 = @"NSThread3";
NSString *threadNameMain = @"NSThreadMain";
NSThread *thread1 = [[NSThread alloc] initWithTarget:self
selector:@selector(doSomething:) object:threadName1];
[thread1 start];
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self
withObject:threadName2];
[self performSelectorInBackground:@selector(doSomething:)
withObject:threadName3];
//執行在主執行緒,waitUntilDone:是否阻塞等待@selector(doSomething:)執行完畢
[self performSelectorOnMainThread:@selector(doSomething:)
withObject:threadNameMain waitUntilDone:YES];
}
- (void) doSomething:(NSObject *)object{
NSLog(@"%@:%@", object,[NSThread currentThread]);
}
複製程式碼
2. NSThread的常用類方法
- 返回當前執行緒
// 當前執行緒
[NSThread currentThread];
NSLog(@"%@",[NSThread currentThread]);
// 如果number=1,則表示在主執行緒,否則是子執行緒
列印結果:<NSThread: 0x608000261380>{number = 1, name = main}。
複製程式碼
- 阻塞休眠
//休眠多久
[NSThread sleepForTimeInterval:2];
//休眠到指定時間
[NSThread sleepUntilDate:[NSDate date]];
複製程式碼
- 類方法補充
//退出執行緒
[NSThread exit];
//判斷當前執行緒是否為主執行緒
[NSThread isMainThread];
//判斷當前執行緒是否是多執行緒
[NSThread isMultiThreaded];
主執行緒的物件
NSThread *mainThread = [NSThread mainThread];
複製程式碼
- NSThread的一些屬性
//執行緒是否在執行
thread.isExecuting;
//是否被取消
thread.isCancelled;
//是否完成
thread.isFinished;
//是否是主執行緒
thread.isMainThread;
//執行緒的優先順序,取值範圍0.0-1.0,預設優先順序0.5,1.0表示最高優先順序,優先順序高,CPU排程的頻率高
thread.threadPriority;
複製程式碼
六、GCD的理解與使用
No.1:GCD的特點
- GCD會自動利用更多的CPU核心
- GCD自動管理執行緒的生命週期(建立執行緒、排程任務、銷燬執行緒等)
- 程式設計師只需要告訴GCD想要如何執行任務,不需要編寫任何執行緒管理程式碼
No.2:GCD的基本概念
2.1. 任務
任務 :就是執行操作的意思,換句話說就是你線上程中執行的那段程式碼。在 GCD 中是放在 block 中的。
執行任務有兩種方式:同步執行(sync)和非同步執行(async)。
兩者的主要區別是:是否等待佇列的任務執行結束,以及是否具備開啟新執行緒的能力。
- 同步執行(sync):
- 同步同步新增任務到指定的佇列中,在新增的任務執行結束之前,會一直等待,直到佇列裡面的任務完成之後再繼續執行
- 只能在當前執行緒中執行任務,不具備開啟新執行緒的能力。
- 非同步執行(async):
- 非同步新增任務到指定的佇列中,它不會做任何等待,可以繼續執行任務。
- 可以在新的執行緒中執行任務,具備開啟新執行緒的能力。
- 非同步是多執行緒的代名詞。
注意:非同步執行(async)雖然具有開啟新執行緒的能力,但是並不一定開啟新執行緒。這跟任務所指定的 佇列型別 有關
2.2. 佇列
佇列(Dispatch Queue): 這裡的佇列執行任務的等待佇列,即用來存放任務的佇列。佇列是一種特殊的線性表,採用 FIFO(先進先出) 的原則,即 新任務總是被插入到佇列的末尾,而讀取任務的時候總是 從佇列的頭部開始讀取 。每讀取一個任務,則從佇列中釋放一個任務。結構圖:
在GCD中有兩種佇列:序列佇列和併發佇列。兩者都符合FIFO原則。
兩者的主要 區別 是:執行順序不同,以及 開啟執行緒數不同 。
- 序列佇列(Serial Dispatch Queue):
- 每次只有一個任務被執行,讓任務一個接著一個地執行(只開啟一個執行緒)。
- 併發佇列(Concurrent Dispatch Queue):
- 可以讓多個任務併發(同時)執行。(可以開啟多個執行緒,並且同時執行任務)。
注意:併發佇列 的併發功能只有在非同步(dispatch_async)函式下才有效
GCD總結:將任務(要線上程中執行的操作block)新增到佇列(自己建立或使用全域性併發佇列),並且制定執行任務的方式(非同步或同步)。
No.3:GCD 的使用步驟
- 建立一個佇列
- 將任務追加到任務的等待佇列中,然後系統就會根據任務型別執行任務
3.1 佇列的建立方法/獲取方法
- 可以使用
dispatch_queue_create
來建立佇列,需要傳入兩個引數,第一個參數列示佇列的 唯一識別符號,用於DEBUG,可為空,推薦使用應用程式ID這種逆序全程域名;第二個參數列示 佇列任務型別,序列或併發佇列。 DISPATCH_QUEUE_SERIAL
表示序列佇列DISPATCH_QUEUE_CONCURRENT
表示併發佇列
// 序列佇列
dispatch_queue_t serialQueue = dispatch_queue_create("com.leejtom.testQueue",
DISPATCH_QUEUE_SERIAL);
// 併發佇列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.leejtom.testQueue",
DISPATCH_QUEUE_CONCURRENT);
複製程式碼
- 對於 序列佇列,GCD提供了一種特殊的序列佇列:主佇列(Main Dispatch Queue)。
- 主佇列複製在主執行緒上排程任務,如果主執行緒上已經有任務正在執行,主佇列會等到主執行緒空閒後再排程任務。
- 所有放到主佇列中任務,都會放到主執行緒中執行。
- 通常是返回主執行緒 更新UI 的時候使用。
- 可使用
dispatch_get_main_queue()
獲得主佇列。
//主佇列的獲取方法
dispatch_queue_t mainQueue = dispatch_get_main_queue();
複製程式碼
- 對於 併發佇列,GCD預設提供了 全域性併發佇列(Global Dispatch Queue)。
- 可以使用
dispatch_get_global_queue
來獲取。- 第一個引數:表示佇列優先順序,一般用
DISPATCH_QUEUE_PRIORITY_DEFAULT
- 第二個引數:使用0
- 第一個引數:表示佇列優先順序,一般用
- 可以使用
//全域性併發佇列的獲取方法
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
複製程式碼
通常我們這樣使用兩者:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 耗時操作放在這裡
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主執行緒進行UI操作
});
});
複製程式碼
3.2 同步/非同步/任務、建立方式
- GCD 提供了同步執行任務的建立方法
dispatch_sync
和非同步執行任務建立方法dispatch_async
。
// 同步執行任務
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
//TO DO
});
// 非同步執行任務
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//TO DO
});
複製程式碼
3.3 GCD的組合方式:
- 同步執行 + 併發佇列
- 非同步執行 + 併發佇列
- 同步執行 + 序列佇列
- 非同步執行 + 序列佇列
- 同步執行 + 主佇列
- 非同步執行 + 主佇列
區別 | 併發佇列 | 序列佇列 | 主佇列 |
---|---|---|---|
同步(sync) | 沒有開啟新執行緒, 序列執行任務 |
沒有開啟新執行緒, 序列執行任務 |
主執行緒呼叫:死鎖卡住不執行; 其他執行緒呼叫:沒有開啟新執行緒, 序列執行任務 |
非同步(async) | 有開啟新執行緒, 併發執行任務 |
有開啟新執行緒(1條), 序列執行任務 |
沒有開啟新執行緒, 序列執行任務 |
No.4. GCD的基本使用
4.1 同步執行 + 併發佇列
- 特點:在當前執行緒中執行任務,不會開啟新執行緒,執行完一個任務,再執行下一個任務。
/**
* 同步執行 + 併發佇列
* 特點:在當前執行緒中執行任務,不會開啟新執行緒,執行完一個任務,再執行下一個任務。
*/
- (void) syncConcurrent {
NSLog(@"currentThread: %@", [NSThread currentThread]);
NSLog(@"syncConcurrent begin");
dispatch_queue_t concurrentQueue = dispatch_queue_create("leejtom.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模擬耗時操作
NSLog(@"task1--%@", [NSThread currentThread]);// 列印當前執行緒
}
});
dispatch_sync(concurrentQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task2--%@", [NSThread currentThread]);
}
});
dispatch_sync(concurrentQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task3--%@", [NSThread currentThread]);
}
});
NSLog(@"syncConcurrent end");
}
複製程式碼
輸入結果為 順序執行,都在主執行緒:
currentThread: <NSThread: 0x115d0ba00>{number = 1, name = main}
syncConcurrent begin
task1--<NSThread: 0x115d0ba00>{number = 1, name = main}
task1--<NSThread: 0x115d0ba00>{number = 1, name = main}
task2--<NSThread: 0x115d0ba00>{number = 1, name = main}
task2--<NSThread: 0x115d0ba00>{number = 1, name = main}
task3--<NSThread: 0x115d0ba00>{number = 1, name = main}
task3--<NSThread: 0x115d0ba00>{number = 1, name = main}
syncConcurrent end
4.2 非同步執行 + 併發佇列
- 特點:可以開啟多個執行緒,任務交替(同時)執行。
/**
*非同步執行 + 併發佇列
*特點:可以開啟多個執行緒,任務交替(同時)執行。
*/
- (void) asyncConcurrent {
NSLog(@"currentThread: %@", [NSThread currentThread]);
NSLog(@"asyncConcurrent begin");
dispatch_queue_t concurrentQueue = dispatch_queue_create("leejtom.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模擬耗時操作
NSLog(@"task1--%@", [NSThread currentThread]);// 列印當前執行緒
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task2--%@", [NSThread currentThread]);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task3--%@", [NSThread currentThread]);
}
});
NSLog(@"asyncConcurrent end");
}
複製程式碼
輸出結果為可以 開啟多個執行緒,任務交替(同時)執行:
currentThread: <NSThread: 0x113e05aa0>{number = 1, name = main}
asyncConcurrent begin
asyncConcurrent end
task3--<NSThread: 0x113d8a720>{number = 3, name = (null)}
task1--<NSThread: 0x113d89e40>{number = 4, name = (null)}
task2--<NSThread: 0x113d83c60>{number = 5, name = (null)}
task3--<NSThread: 0x113d8a720>{number = 3, name = (null)}
task2--<NSThread: 0x113d83c60>{number = 5, name = (null)}
task1--<NSThread: 0x113d89e40>{number = 4, name = (null)}
在 非同步執行 + 併發佇列 中可以看出:
- 除了當前執行緒(主執行緒),系統又開啟了3個執行緒,並且任務是交替/同時執行的。(非同步 執行具備開啟新執行緒的能力。且 併發佇列 可開啟多個執行緒,同時執行多個任務)。
- 所有任務是在列印的 asyncConcurrent begin 和 asyncConcurrent end 之後才執行的。說明當前執行緒沒有等待,而是直接開啟了新執行緒,在新執行緒中執行任務(非同步執行不做等待,可以繼續執行任務)。
4.3 同步執行 + 序列佇列
- 特點:不會開啟新執行緒,在當前執行緒執行任務。任務是序列的,執行完一個任務,再執行下一個任務。
/**
* 同步執行 + 序列佇列
* 特點:不會開啟新執行緒,在當前執行緒執行任務。
* 任務是序列的,執行完一個任務,再執行下一個任務。
*/
- (void) syncSerial {
NSLog(@"currentThread: %@", [NSThread currentThread]);
NSLog(@"syncSerial begin");
dispatch_queue_t serialQueue = dispatch_queue_create("leejtom.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模擬耗時操作
NSLog(@"task1--%@", [NSThread currentThread]);// 列印當前執行緒
}
});
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task2--%@", [NSThread currentThread]);
}
});
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task3--%@", [NSThread currentThread]);
}
});
NSLog(@"syncSerial end");
}
複製程式碼
輸出結果為 順序執行,都在主執行緒:
currentThread: <NSThread: 0x11fe04970>{number = 1, name = main}
syncSerial begin
task1--<NSThread: 0x11fe04970>{number = 1, name = main}
task1--<NSThread: 0x11fe04970>{number = 1, name = main}
task2--<NSThread: 0x11fe04970>{number = 1, name = main}
task2--<NSThread: 0x11fe04970>{number = 1, name = main}
task3--<NSThread: 0x11fe04970>{number = 1, name = main}
task3--<NSThread: 0x11fe04970>{number = 1, name = main}
syncSerial end
4.4 非同步執行 + 序列佇列
- 會開啟新執行緒,但是因為任務是序列的,執行完一個任務,再執行下一個任務
/**
* 非同步執行 + 序列佇列
* 特點:會開啟新執行緒,但是因為任務是序列的,執行完一個任務,再執行下一個任務。
*/
- (void) asyncSerial {
NSLog(@"currentThread: %@", [NSThread currentThread]);
NSLog(@"asyncSerial begin");
dispatch_queue_t serialQueue = dispatch_queue_create("leejtom.testQueue",DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模擬耗時操作
NSLog(@"task1--%@", [NSThread currentThread]);// 列印當前執行緒
}
});
dispatch_async(serialQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task2--%@", [NSThread currentThread]);
}
});
dispatch_async(serialQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task3--%@", [NSThread currentThread]);
}
});
NSLog(@"asyncSerial end");
}
複製程式碼
輸出結果為 順序執行,有不同執行緒:
currentThread: <NSThread: 0x101005730>{number = 1, name = main}
asyncSerial begin
asyncSerial end
task1--<NSThread: 0x1010ab140>{number = 3, name = (null)}
task1--<NSThread: 0x1010ab140>{number = 3, name = (null)}
task2--<NSThread: 0x1010ab140>{number = 3, name = (null)}
task2--<NSThread: 0x1010ab140>{number = 3, name = (null)}
task3--<NSThread: 0x1010ab140>{number = 3, name = (null)}
task3--<NSThread: 0x1010ab140>{number = 3, name = (null)}
- 開啟了一條新執行緒( 非同步執行 具備開啟新執行緒的能力,序列佇列 只開啟一個執行緒)。
- 所有任務是在列印的 asyncSerial begin 和 asyncSerial end 之後才開始執行的(非同步執行不會做任何等待,可以繼續執行任務)。
- 任務是按順序執行的( 序列佇列 每次只有一個任務被執行,任務一個接一個按順序執行)。
4.5 同步執行 + 主佇列
同步執行 + 主佇列
在不同執行緒中呼叫結果也是不一樣,在主執行緒中呼叫會出現死鎖,而在其他執行緒中則不會。
4.5.1 同步執行 + 主佇列
- 主佇列:GCD自帶的一種特殊的序列佇列
- 所有放在主佇列中的任務,都會放到主執行緒中執行
- 可使用
dispatch_get_main_queue()
獲得主佇列
- 發生死鎖,互等卡住不執行,程式崩潰
/**
* 同步執行 + 主佇列
* 特點(主執行緒呼叫):互等卡住不執行。
* 特點(其他執行緒呼叫):不會開啟新執行緒,執行完一個任務,再執行下一個任務。
*/
- (void) syncMain {
NSLog(@"currentThread: %@", [NSThread currentThread]);
NSLog(@"syncMain begin");
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模擬耗時操作
NSLog(@"task1--%@", [NSThread currentThread]);// 列印當前執行緒
}
});
dispatch_sync(mainQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task2--%@", [NSThread currentThread]);
}
});
dispatch_sync(mainQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task3--%@", [NSThread currentThread]);
}
});
NSLog(@"syncMain end");
}
複製程式碼
輸出結果 發生死鎖,互等卡住不執行,程式崩潰:
currentThread: <NSThread: 0x101201f70>{number = 1, name = main} syncMain begin
(lldb)
這是因為我們在主執行緒中執行syncMain
方法,相當於把syncMain
任務放到了主執行緒的佇列中。而同步執行(dispatch_sync)
會等待當前佇列中的任務執行完畢,才會接著執行。那麼當我們把任務1追加到主佇列中,任務1就在等待主執行緒處理完syncMain
任務。而syncMain
任務需要等待任務1
執行完畢,才能接著執行。
那麼,現在的情況就是syncMain
任務和任務1
都在等對方執行完畢。這樣大家互相等待,所以就卡住了,所以我們的任務執行不了,而且syncMain end
也沒有列印。
要是如果不在主執行緒中呼叫,而在其他執行緒中呼叫會如何呢?
4.5.2 在其他執行緒中呼叫同步執行 + 主佇列
- 不會開啟新執行緒,執行完一個任務,再執行下一個任務
// 使用 NSThread 的 detachNewThreadSelector 方法會建立執行緒,並自動啟動執行緒執行selector 任務
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
複製程式碼
輸出結果
currentThread: <NSThread: 0x121d96740>{number = 3, name = (null)}
syncMain begin
task1--<NSThread: 0x121d0ba20>{number = 1, name = main}
task1--<NSThread: 0x121d0ba20>{number = 1, name = main}
task2--<NSThread: 0x121d0ba20>{number = 1, name = main}
task2--<NSThread: 0x121d0ba20>{number = 1, name = main}
task3--<NSThread: 0x121d0ba20>{number = 1, name = main}
task3--<NSThread: 0x121d0ba20>{number = 1, name = main}
syncMain end
在其他執行緒中使用同步執行 + 主佇列
可看到:
所有任務都是在主執行緒(非當前執行緒)中執行的,沒有開啟新的執行緒(所有放在主佇列中的任務,都會放到主執行緒中執行)。
所有任務都在列印的syncMain begin
和syncMain end
之間執行(同步任務需要等待佇列的任務執行結束)。
任務是按順序執行的(主佇列是序列佇列,每次只有一個任務被執行,任務一個接一個按順序執行)。
為什麼現在就不會卡住了呢?
因為syncMain
任務放到了其他執行緒裡,而任務1、任務2、任務3都在追加到主佇列中,這三個任務都會在主執行緒中執行。syncMain
任務在其他執行緒中執行到追加任務1到主佇列中,因為主佇列現在沒有正在執行的任務,所以,會直接執行主佇列的任務1,等任務1執行完畢,再接著執行任務2、任務3。所以這裡不會卡住執行緒。
4.6 非同步執行 + 主佇列
- 只在主執行緒中執行任務,執行完一個任務,再執行下一個任務。
/**
* 非同步執行 + 主佇列
* 特點:只在主執行緒中執行任務,執行完一個任務,再執行下一個任務
*/
- (void) asyncMain {
NSLog(@"currentThread: %@", [NSThread currentThread]);
NSLog(@"asyncMain begin");
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模擬耗時操作
NSLog(@"task1--%@", [NSThread currentThread]);// 列印當前執行緒
}
});
dispatch_async(mainQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task2--%@", [NSThread currentThread]);
}
});
dispatch_async(mainQueue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];
NSLog(@"task3--%@", [NSThread currentThread]);
}
});
NSLog(@"asyncMain end");
}
複製程式碼
輸出結果:
currentThread: <NSThread: 0x100e05980>{number = 1, name = main}
asyncMain begin
asyncMain end
task1--<NSThread: 0x100e05980>{number = 1, name = main}
task1--<NSThread: 0x100e05980>{number = 1, name = main}
task2--<NSThread: 0x100e05980>{number = 1, name = main}
task2--<NSThread: 0x100e05980>{number = 1, name = main}
task3--<NSThread: 0x100e05980>{number = 1, name = main}
task3--<NSThread: 0x100e05980>{number = 1, name = main}
- 所有任務都是在當前執行緒(主執行緒)中執行的,並沒有開啟新的執行緒(雖然非同步執行具備開啟執行緒的能力,但因為是主佇列,所以所有任務都在主執行緒中)。
- 所有任務是在列印的syncConcurrent---begin和syncConcurrent---end之後才開始執行的(非同步執行不會做任何等待,可以繼續執行任務)。 任務是按順序執行的(因為主佇列是序列佇列,每次只有一個任務被執行,任務一個接一個按順序執行)。
5. GCD 執行緒間的通訊
在iOS開發過程中,我們一般在主執行緒裡邊進行UI重新整理,例如:點選、滾動、拖拽等事件。我們通常把一些耗時的操作放在其他執行緒,比如說圖片下載、檔案上傳等耗時操作。而當我們有時候在其他執行緒完成了耗時操作時,需要回到主執行緒,那麼就用到了執行緒之間的通訊。
/**
* 執行緒間通訊
*/
- (void)communication {
// 獲取全域性併發佇列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 獲取主佇列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 非同步追加任務
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"1---%@",[NSThread currentThread]); // 列印當前執行緒
}
// 回到主執行緒
dispatch_async(mainQueue, ^{
// 追加在主執行緒中執行的任務
[NSThread sleepForTimeInterval:2]; // 模擬耗時操作
NSLog(@"2---%@",[NSThread currentThread]); // 列印當前執行緒
});
});
}
複製程式碼
1---<NSThread: 0x159e97a40>{number = 3, name = (null)}
1---<NSThread: 0x159e97a40>{number = 3, name = (null)}
2---<NSThread: 0x159e08c20>{number = 1, name = main}
- 可以看到在其他執行緒中先執行任務,執行完了之後回到主執行緒執行主執行緒的相應操作。
七、NSOperation的理解與使用
1. NSOperation簡介
NSOperation是基於GCD之上的更高一層封裝,NSOperation需要配合NSOperationQueue來實現多執行緒。
NSOperatino實現多執行緒的步驟如下:
- 建立任務:先將需要執行的操作封裝到
NSOperation
物件中。- 建立佇列:建立
NSOperationQueue
。- 將任務加入到佇列中:將
NSOperation
物件新增到NSOperationQueue
中。
需要注意的是,NSOperation是個抽象類,實際運用時中需要使用它的子類,有三種方式:
- 使用子類NSInvocationOperation
- 使用子類NSBlockOperation
- 定義繼承自NSOperation的子類,通過實現內部相應的方法來封裝任務。
2. NSOperation的三種建立方式
- NSInvocationOperation
- NSBlockOperation
- 運用繼承自NSOperation的子類
2.1 NSInvocationOperation的使用
建立NSInvocationOperation物件
並關聯方法,之後start。
- (void) createNSOperation {
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation) object:nil];
[invocationOperation start];
}
- (void) invocationOperation {
NSLog(@"currentThread: %@", [NSThread currentThread]);
}
複製程式碼
輸出結果 程式在主執行緒執行,沒有開啟新執行緒:
currentThread: <NSThread: 0x143d0b880>{number = 1, name = main}
2.2 通過addExecutionBlock
這個方法可以讓NSBlockOperation實現多執行緒。
- (void) testNSBlockOperationExecution {
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"main task = >currentThread: %@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
NSLog(@"task1 = >currentThread: %@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
NSLog(@"task2 = >currentThread: %@", [NSThread currentThread]);
}];
[blockOperation addExecutionBlock:^{
NSLog(@"task3 = >currentThread: %@", [NSThread currentThread]);
}];
[blockOperation start];
}
複製程式碼
結果:NSBlockOperation建立時block
中的任務是在主執行緒
執行,而運用addExecutionBlock
加入的任務是在子執行緒
執行的。
main task = >currentThread: <NSThread: 0x10160b760>{number = 1, name = main}
task1 = >currentThread: <NSThread: 0x101733690>{number = 3, name = (null)}
task2 = >currentThread: <NSThread: 0x10160b760>{number = 1, name = main}
task3 = >currentThread: <NSThread: 0x101733690>{number = 3, name = (null)}
2.3 運用繼承自NSOperation的子類 首先我們定義一個繼承自NSOperation的類,然後重寫它的main方法。
// JTOperation.m
#import "JTOperation.h"
@implementation JTOperation
- (void)main {
for (int i = 0; i < 3; i++) {
NSLog(@"NSOperation的子類:%@",[NSThread currentThread]);
}
}
//呼叫
- (void)testJTOperation {
JTOperation *operation = [[JTOperation alloc]init];
[operation start];
}
複製程式碼
執行結果 在主執行緒執行:
NSOperation的子類:<NSThread: 0x101605ba0>{number = 1, name = main}
NSOperation的子類:<NSThread: 0x101605ba0>{number = 1, name = main}
NSOperation的子類:<NSThread: 0x101605ba0>{number = 1, name = main}
3. 佇列NSOperationQueue
NSOperationQueue有兩種佇列:主佇列
、其他佇列
。其他佇列包含了 序列和併發。
- 主佇列,主佇列上的任務是在主執行緒執行的。
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
複製程式碼
- 其他佇列(非主佇列),加入到'非佇列'中的任務預設就是併發,開啟多執行緒。
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
複製程式碼
注意:
- 非主佇列(其他佇列)可以實現序列或並行。
- 佇列NSOperationQueue有一個引數叫做最大併發數:
@property NSInteger maxConcurrentOperationCount;
複製程式碼
- maxConcurrentOperationCount預設為-1,直接併發執行,所以加入到‘非佇列’中的任務預設就是
併發,開啟多執行緒
。
static const NSInteger NSOperationQueueDefaultMaxConcurrentOperationCount = -1;
複製程式碼
- 當maxConcurrentOperationCount為1時,則表示不開執行緒,也就是
序列
。- 當maxConcurrentOperationCount大於1時,進行
併發執行
。- 系統對最大併發數有一個限制,所以即使程式設計師把maxConcurrentOperationCount設定的很大,系統也會自動調整。所以把最大併發數設定的很大是沒有意義的。
4. NSOperation + NSOperationQueue
把任務加入佇列,這才是NSOperation的常規使用方式。
- addOperation新增任務到佇列
- (void)testNSOperationQueue {
//建立佇列,預設併發
NSOperationQueue *queuq = [[NSOperationQueue alloc] init];
//建立NSInvocationOperation
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAddOperation) object:nil];
//建立NSBlockOperation
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"NSBlockOperation: %@", [NSThread currentThread]);
}
}];
//addOperation
[queuq addOperation:invocationOperation];
[queuq addOperation:blockOperation];
}
- (void)operationAddOperation {
NSLog(@"NSInvocationOperation: %@", [NSThread currentThread]);
}
複製程式碼
執行結果如下,任務確實是在子執行緒中執行。
NSInvocationOperation: <NSThread: 0x101a42bf0>{number = 3, name = (null)}
NSBlockOperation: <NSThread: 0x101a42bf0>{number = 3, name = (null)}
NSBlockOperation: <NSThread: 0x101a42bf0>{number = 3, name = (null)}
NSBlockOperation: <NSThread: 0x101a42bf0>{number = 3, name = (null)}
- 運用最大併發數實現序列 運用佇列的屬性maxConcurrentOperationCount(最大併發數)來實現序列,值需要把它設定為1就可以了,下面我們通過程式碼驗證一下。
- (void)testMaxConcurrentOperationCount {
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
queue.maxConcurrentOperationCount = 1;
// queue.maxConcurrentOperationCount = 2;
[queue addOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"task1: %@",[NSThread currentThread]);
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"task2: %@",[NSThread currentThread]);
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"task3: %@",[NSThread currentThread]);
}
}];
}
複製程式碼
執行結果如下,當最大併發數為1的時候,雖然開啟了執行緒,但是任務是順序執行的,所以實現了序列:
task1: <NSThread: 0x11be67dc0>{number = 3, name = (null)}
task1: <NSThread: 0x11be67dc0>{number = 3, name = (null)}
task1: <NSThread: 0x11be67dc0>{number = 3, name = (null)}
task2: <NSThread: 0x11bdb1bf0>{number = 4, name = (null)}
task2: <NSThread: 0x11bdb1bf0>{number = 4, name = (null)}
task2: <NSThread: 0x11bdb1bf0>{number = 4, name = (null)}
task3: <NSThread: 0x11be67dc0>{number = 3, name = (null)}
task3: <NSThread: 0x11be67dc0>{number = 3, name = (null)}
task3: <NSThread: 0x11be67dc0>{number = 3, name = (null)}
當最大併發數變為2,會發現任務就變成了併發執行:
task1: <NSThread: 0x10077ca60>{number = 3, name = (null)}
task2: <NSThread: 0x10077d3e0>{number = 4, name = (null)}
task2: <NSThread: 0x10077d3e0>{number = 4, name = (null)}
task2: <NSThread: 0x10077d3e0>{number = 4, name = (null)}
task1: <NSThread: 0x10077ca60>{number = 3, name = (null)}
task1: <NSThread: 0x10077ca60>{number = 3, name = (null)}
task3: <NSThread: 0x10077d3e0>{number = 4, name = (null)}
task3: <NSThread: 0x10077d3e0>{number = 4, name = (null)}
task3: <NSThread: 0x10077d3e0>{number = 4, name = (null)}
5. NSOperation的其他操作
- 取消佇列NSOperationQueue的所有操作,NSOperationQueue物件方法
- (void)cancelAllOperations
複製程式碼
- 取消NSOperation的某個操作,NSOperation物件方法
- (void)cancel
複製程式碼
- 使佇列暫停或繼續
// 暫停佇列
[queue setSuspended:YES];
複製程式碼
- 判斷佇列是否暫停
- (BOOL)isSuspended
複製程式碼
注意:暫停和取消不是立刻取消當前操作,而是等當前的操作執行完之後不再進行新的操作。
6. NSOperation的操作依賴
NSOperation有一個非常好用的方法,就是操作依賴。可以從字面意思理解:某一個操作(operation2)依賴於另一個操作(operation1),只有當operation1執行完畢,才能執行operation2,這時,就是操作依賴大顯身手的時候了。
- (void)testAddDependency {
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"operation1: %@",[NSThread currentThread]);
}
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"operation2: %@",[NSThread currentThread]);
}
}];
[operation2 addDependency:operation1];
[queue addOperation:operation1];
[queue addOperation:operation2];
}
複製程式碼
輸出結果:操作2總是在操作1之後執行,成功驗證了上面的說法.
operation1: <NSThread: 0x103d07d40>{number = 3, name = (null)}
operation1: <NSThread: 0x103d07d40>{number = 3, name = (null)}
operation1: <NSThread: 0x103d07d40>{number = 3, name = (null)}
operation2: <NSThread: 0x103d07d40>{number = 3, name = (null)}
operation2: <NSThread: 0x103d07d40>{number = 3, name = (null)}
operation2: <NSThread: 0x103d07d40>{number = 3, name = (null)}
iOS多執行緒全套
iOS多執行緒:『GCD』詳盡總結
linux執行緒結束函式對比說明join、cancel、kill、exit等