iOS多執行緒(Pthread、NSThread、GCD、NSOperation)

LeeJTom發表於2018-03-16

本文純屬個人讀書筆記。

本文主要分為以下模組:

一、知識點
二、執行緒的生命週期:新建 - 就緒 - 執行 - 阻塞 - 死亡
三、多執行緒的四種方案: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(先進先出) 的原則,即 新任務總是被插入到佇列的末尾,而讀取任務的時候總是 從佇列的頭部開始讀取 。每讀取一個任務,則從佇列中釋放一個任務。結構圖:

iOS多執行緒(Pthread、NSThread、GCD、NSOperation)

在GCD中有兩種佇列:序列佇列和併發佇列。兩者都符合FIFO原則。
兩者的主要 區別 是:執行順序不同,以及 開啟執行緒數不同

  • 序列佇列(Serial Dispatch Queue):
    • 每次只有一個任務被執行,讓任務一個接著一個地執行(只開啟一個執行緒)。
      iOS多執行緒(Pthread、NSThread、GCD、NSOperation)
  • 併發佇列(Concurrent Dispatch Queue):
    • 可以讓多個任務併發(同時)執行。(可以開啟多個執行緒,並且同時執行任務)。

iOS多執行緒(Pthread、NSThread、GCD、NSOperation)

注意併發佇列 的併發功能只有在非同步(dispatch_async)函式下才有效

GCD總結:將任務(要線上程中執行的操作block)新增到佇列(自己建立或使用全域性併發佇列),並且制定執行任務的方式(非同步或同步)。

No.3:GCD 的使用步驟

  1. 建立一個佇列
  2. 將任務追加到任務的等待佇列中,然後系統就會根據任務型別執行任務

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的組合方式:

  1. 同步執行 + 併發佇列
  2. 非同步執行 + 併發佇列
  3. 同步執行 + 序列佇列
  4. 非同步執行 + 序列佇列
  5. 同步執行 + 主佇列
  6. 非同步執行 + 主佇列
區別 併發佇列 序列佇列 主佇列
同步(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 beginasyncConcurrent 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 beginasyncSerial 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 beginsyncMain 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實現多執行緒的步驟如下:

  1. 建立任務:先將需要執行的操作封裝到NSOperation物件中。
  2. 建立佇列:建立NSOperationQueue
  3. 將任務加入到佇列中:將NSOperation物件新增到NSOperationQueue中。

需要注意的是,NSOperation是個抽象類,實際運用時中需要使用它的子類,有三種方式:

  1. 使用子類NSInvocationOperation
  2. 使用子類NSBlockOperation
  3. 定義繼承自NSOperation的子類,通過實現內部相應的方法來封裝任務。

2. NSOperation的三種建立方式

  1. NSInvocationOperation
  2. NSBlockOperation
  3. 運用繼承自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];
複製程式碼

注意:

  1. 非主佇列(其他佇列)可以實現序列或並行。
  2. 佇列NSOperationQueue有一個引數叫做最大併發數:
@property NSInteger maxConcurrentOperationCount;
複製程式碼
  1. maxConcurrentOperationCount預設為-1,直接併發執行,所以加入到‘非佇列’中的任務預設就是併發,開啟多執行緒
static const NSInteger NSOperationQueueDefaultMaxConcurrentOperationCount = -1;
複製程式碼
  1. 當maxConcurrentOperationCount為1時,則表示不開執行緒,也就是序列
  2. 當maxConcurrentOperationCount大於1時,進行併發執行
  3. 系統對最大併發數有一個限制,所以即使程式設計師把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)}

文章原始碼GitHub地址

iOS多執行緒全套
iOS多執行緒:『GCD』詳盡總結
linux執行緒結束函式對比說明join、cancel、kill、exit等

相關文章