iOS 多執行緒

kim_jin發表於2017-12-26

iOS 多執行緒.png

概念

  • 程式(Process): 簡單理解就是一個任務。對於iOS系統來說,一個程式可以認為就是一個app

  • 執行緒(Thread): 執行緒就是在程式中執行的子任務。共享一個程式中的所有資源(記憶體)。

一個程式可以包括多個執行緒。雖然執行緒共享程式中的資源,但是當一個資源被其中一個執行緒佔用時,其他執行緒就沒辦法使用,需要等待其釋放出來。

  • 同步(Sync): 按照順序完成某事,會阻塞當前執行緒

  • 非同步(Async): 同時做好幾件事,呼叫後立即返回,不阻塞執行緒

執行緒的基本概念中,我個人覺得比較容易混淆的就是併發和並行這個兩個:

  • 併發(Concurrency): 將相互獨立的執行過程綜合到一起。

  • 並行(Parallelism): 同時執行任務

  • 序列(Serial): 一次只能執行一個任務,必須等當前任務執行完畢後才能執行下一個任務

注意併發和並行之間的區別 併發指的是同時處理很多事情,並行指同時能夠完成很多事情。前者重點是組合,我覺得可以理解為形容詞,提供的是一種方式,後者重點是執行,這裡就理解為動詞,提供的是一種解決。兩者相關,但是具體含義不同。

三種方式

在iOS中,建立執行緒的方式基本上有三種:

  • NSThread

  • NSOperation

  • GCD

這三種方法的封裝性從上往下遞增,也就是使用起來GCD最為方便,其次是NSOperation,最後是NSThread

NSThread

NSThread是最靠近底層的物件,所以使用起來比較麻煩,需要自己管理執行緒的宣告週期,並且也沒有實現執行緒狀態等相關的功能。NSThread通常會用於一些比較輕量級的任務,比方iOS工程師都比較熟悉的performSelector:方法,就是屬於NSThread的範疇。

如果想要實現自己的NSThread的話,可以繼承NSThread並且重寫main方法。如果重寫了main的話,在呼叫其他繼承的方法時就不用再加上super

建立執行緒

建立一個NSThread物件可以使用init或者是initWithTarget:selector:object:。第二個方法的最後一個引數用於傳入selector物件所需的引數。

啟動執行緒

  • + detachNewThreadSelector:toTarget:withObject:分發新的執行緒並且將指定的方法作為執行緒的入口。在執行期間,target和object都會被retained,當方法體執行完畢之後就會釋放。

  • + detachNewThreadWithBlock:這個方法跟上面的類似,只不過是將selector變成了block來執行而已。

  • - start開啟執行緒

  • - main為執行緒的入口點。不能直接呼叫,用於繼承NSThread實現自己的執行緒類時,重寫。

在啟動執行緒的時候,如果該執行緒是app首次新增的執行緒物件的話,那麼前三個方法會傳送NSWillBecomeMultiThreadNotification通知。

停止執行緒

  • + sleepUntilDate: 執行緒休眠,直到引數指定的時間點。在阻塞執行緒期間,不會出現任何的runloop程式。

  • + sleepForTimeInterval:在指定的時間內進行執行緒休眠

  • + exit停止當前執行緒。exit方法會呼叫currentThread來獲取當前的執行緒,然後停止之。在停止當前執行緒之前,會將當前的執行緒作為objectNSThreadWillExitNotificationpost出去。exit方法可能會造成crash或者記憶體洩漏。慎用!

  • - cancel會取消當前的執行緒工作。但是需要注意的是cancel有可能並不會馬上迫使當前的執行緒停下來,而是在當前執行緒的工作完成以後,執行緒才會停下來。如果執行緒工作已經完成的話,呼叫cancel並不會有任何作用。在一個操作佇列中,如果對某個操作呼叫了cancel方法,則在該操作執行完畢後,在該操作之後的其他操作將不會執行cancel方法實際上只是將執行緒標記為cancel狀態,並不會停止執行緒。通常的用法是對執行緒物件呼叫方法後,根據執行緒的狀態來判斷應該進行的下一步操作。

    e.g.:

    for (int i = 0; i < 100; i++) {
    	if ([NSThread currentThread].isCancelled) 
    		return;
    	NSLog(@"%@ - %d", [NSThread currentThread], i);
        if (i == 50) {
            [[NSThread currentThread] cancel];
        }
    }
    複製程式碼

執行緒的執行狀態

  • executing表明執行緒是否正在執行

  • finished表明執行緒是否執行完畢

  • cancelled表明執行緒是否被置為取消

與主執行緒相關的屬性

  • isMainThread. NSThread有兩個isMainThread的只讀屬性,其中一個是型別屬性,表明當前的執行緒是否為主執行緒;另一個為例項屬性,表明接收者是否為主執行緒。即
[NSThread isMainThread];
		
// or
[self.thread isMainThread];
複製程式碼
  • mainThread類屬性,用於獲取主執行緒物件

執行環境查詢

  • + isMultiThread表明app是否為多執行緒應用。假如通過detachNewThreadSelector:toTarget:withObject:為主執行緒分配了新的工作執行緒或者是呼叫了某個執行緒的start方法的話,則會返回YES。但是如果呼叫瞭如POSIX或者多程式服務等非Cocoa的API來分配執行緒的話,雖然也是有新的執行緒在跑,但是仍然會返回NO

  • currentThread獲取當前所在的執行緒

  • callStackReturnAddresses獲取呼叫棧的記憶體地址

  • callStackSymbols獲取呼叫棧的標誌位

後兩個屬性會比較常用於分析crash log

執行緒屬性相關

  • threadDictionary 執行緒物件的屬性集合,可以用來儲存與執行緒相關的資料。

e.g.:

[self.thread.threadDictionary setValue:@"value1" forKey:@"key1"];
複製程式碼
  • name 執行緒名稱

  • stackSize 執行緒棧區的size。以byte為單位,且大小必須為4KB的倍數。需要更改此值的話,必須線上程開始之前進行修改。

執行緒優先順序

  • + threadPriority 當前執行緒的優先順序。範圍為0.0 ~ 1.0(低到高)。一般是0.5,但是具體的優先順序由核心所決定

  • threadPriority 執行緒優先順序。詳細檢視threadPriority的話,會發現文件中標示即將廢棄掉這個屬性,使用iOS 8之後出現的qualityOfService來替代標示優先順序。

    qualityOfService有5個列舉值:

    typedef NS_ENUM(NSInteger, NSQualityOfService) {
    /* 最高優先順序,用於使用者的UI互動和螢幕內容繪製 */
    NSQualityOfServiceUserInteractive = 0x21,
    
    /* 用於執行一些需要立即獲取結果的任務 */
    NSQualityOfServiceUserInitiated = 0x19,
    
    /* 用於執行並不需要立即返回的任務 */
    NSQualityOfServiceUtility = 0x11,
    
    /* 後臺優先順序用於處理完全不急的任務 */
    NSQualityOfServiceBackground = 0x09,
    
    /* 預設優先順序 */
    NSQualityOfServiceDefault = -1
    複製程式碼

} ```

  • + setThreadPriority: 設定當前執行緒的優先順序

NSOperation

相比NSThread來說,NSOperation雖然是個抽象類,但是我們可以更加註重事件的邏輯實現,而不用過多關注執行緒的週期。因為NSOperation是個抽象類,我們需要繼承並實現相關的方法來實現自己的類,如果嫌麻煩的話,也可以使用系統提供的NSInvocationOperation或者是NSBlockOperation. 跟NSThread物件一樣,呼叫start方法的話,NSOperation物件就會開始執行自己的任務,但是對於NSOperation物件來說,手動執行操作會加重負擔,因為它將一個靜默狀態的操作繞過了ready狀態直接就開始執行,會有較多的消耗。So,一般情況下,都會和NSOperationQueue配合使用。

特點

  • 執行緒依賴

    比方說現在有個需求是從網路上download檔案,但是在download之前需要先登入,也就是說下載的操作依賴於登入,需要在登入之後才能進行download。執行緒依賴就是類似如此,佇列中的操作以指定好的順序進行執行。指定的操作只能在其依賴的操作都執行完畢後,才會進入到ready狀態中。

  • KVO屬性觀察

  • 執行緒安全

    NSThread所不同的是,我們可以安全地在多個執行緒呼叫NSOperation的方法,不用新增為物件的訪問器新增額外的鎖。在實現自己的NSOperation類時,需要保證所重寫的方法的執行緒安全性。

  • 非同步 vs 同步

    如果是手動來執行操作物件的話,就能夠自定義操作的執行方式 -- 非同步或者是同步。NSOperation的預設的執行方式是同步執行。同步操作的話,不會在當前任務的執行緒上建立另一條執行緒來執行,一旦呼叫了start方法,就會立馬在當前執行緒上執行任務

    非同步方式的話,呼叫start方法後會立馬返回,不管其對應的任務是否已經執行完成。因為非同步的operation物件只負責將任務新增到另外的執行緒中。Operation可以通過直接啟動新執行緒,呼叫非同步方法或者提交block給執行佇列來執行任務。

** NSOperation相對於GCD來說,勝線上程依賴和執行緒的執行狀態的管理 **

NSInvocationOperation、NSBlockOperation和自定義Operation

NSInvocationOperationNSBlockOperationNSOperation這個抽象類的實現,前者使用方法作為引數,後者使用block。對這兩者來說,如果手動呼叫start方法的話,都為同步執行任務:

NSInvocationOperation *operation1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil];
[operation1 start];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
	NSLog(@"Operation 2: %@ -- 2", [NSThread currentThread]);
}];
[operation2 start];
複製程式碼

不過,當NSBlockOperation的任務數多餘1的時候,會變為非同步執行:

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
	NSLog(@"Operation 2: %@ -- 2", [NSThread currentThread]);
}];
[operation2 addExecutionBlock:^{
	NSLog(@"Operation 2: %@ -- 3", [NSThread currentThread]);
}];
[operation2 start];
複製程式碼

執行上面這段程式碼,你會發現operation2的兩個block在不同的執行緒上執行,並不會阻塞執行緒。所以是為非同步執行。

如果系統提供的這兩種型別的物件都滿足不了需求的話,就需要自己繼承NSOperation來實現了。其中實現的方式分為非併發和併發兩種型別(Apple的官網上是說nonconcurrent和concurrent,但是在屬性中concurrent卻建議用asynchronous替換。)

  • 非併發操作

    非併發操作的話簡單點,一般情況下只需實現main和初始化方法即可。

    // CustomNonconcurrentOperation.h
    @interface CustomNonconcurrentOperation : NSOperation
    
    @property (nonatomic, assign) NSInteger num;
    
    - (instancetype)initWithNum:(NSInteger)num;
    
    @end
    
    // CustomNonconcurrentOperation.m
    @implementation CustomNonconcurrentOperation
    
    - (instancetype)initWithNum:(NSInteger)num {
    	self = [super init];
    	if (self) {
        	_num = num;
    	}
    	return self;
    }
    
    - (void)main {
    	@try {
        	BOOL isDone = NO;
    
        	while (!self.isCancelled && !isDone) {
            	NSLog(@"%d -- %@", self.num--, [NSThread currentThread]);
            	if (self.num == 0) {
                	isDone = YES;
            	}
        	}
    	} @catch (NSException *exception) {
        NSLog(@"%@", exception.description);
    	} 
    }
    
    @end
    複製程式碼
  • 併發操作

    需要重寫的方法有:

    方法
    start 必須重寫。不能在裡面呼叫super
    main 可選。用於實現任務的執行。建議把任務的實現放在main方法中,而不是start中
    isExecuting isFinished 必須重寫。維持相關的屬性,以便其他物件能夠監聽相關的屬性,並能夠生成相對應的KVO通知
    isConcurrent 必須重寫,返回結果為YES
    // CustomConcurrentOperation.h
    @interface CustomConcurrentOperation : NSOperation {
    	BOOL _executing;
    	BOOL _finished;
    }
    
    @property (nonatomic, assign) NSInteger num;
    
    - (instancetype)initWithNum:(NSInteger)num;
    
    - (void)completeOperation;
    
    @end
    
    //CustomConcurrentOperation.m
    @implementation CustomConcurrentOperation
    
    - (instancetype)initWithNum:(NSInteger)num {
    	self = [super init];
    	if (self) {
        	self.num = num;
        	_executing = NO;
        	_finished = NO;
    	}
    	return self;
    }
    
    - (BOOL)isConcurrent {
    	return YES;
    }
    
    - (BOOL)isExecuting {
    	return _executing;
    }
    
    - (BOOL)isFinished {
    	return _finished;
    }
    
    - (void)start {
    	// 在開啟之前都需要檢查狀態
    	if (self.isCancelled) {
        	[self willChangeValueForKey:@"isFinished"];
        	_finished = YES;
        	[self didChangeValueForKey:@"isFinished"];
        	return;
    	}
    
    	[self willChangeValueForKey:@"isExcuting"];
    	[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    	_executing = YES;
    	[self didChangeValueForKey:@"isExcuting"];
    }
    
    - (void)main {
    	@try {
        	while (!self.isCancelled && !self.isFinished) {
            	NSLog(@"%d -- %@", self.num--, [NSThread currentThread]);
            	if (self.num == 0) {
                	[self completeOperation];
            	}
        	}
    	} @catch (NSException *exception) {
        	NSLog(@"%@", exception.description);
    	}
    }
    
    - (void)completeOperation {
    	[self willChangeValueForKey:@"isFinished"];
    	[self willChangeValueForKey:@"isExecuting"];
    
    	_executing = NO;
    	_finished = YES;
    
    	[self didChangeValueForKey:@"isExecuting"];
    	[self didChangeValueForKey:@"isFinished"];
    }
    
    @end
    複製程式碼

NSOperationQueue

NSOperationQueue如名稱一樣,就是一個佇列。和NSOperation一起使用。經常用到的NSOperationQueue的屬性有個maxConcurrentOperationCount。該屬性用於設定佇列的最大併發運算元。一開始我以為將maxConcurrentOperationCount設定為1,佇列就變成了一個序列佇列。但是實際上,NSOperationQueue就算將併發運算元改為1,仍然是一個並行佇列。根據Apple的文件:

Operation queues usually provide the threads used to run their operations. Operation queues use the libdispatch library (also known as Grand Central Dispatch) to initiate the execution of their operations. As a result, operations are always executed on a separate thread, regardless of whether they are designated as asynchronous or synchronous operations.

佇列中的操作都是在獨立的執行緒中進行操作。在Stack Overflow上也有不少人認為併發運算元為1時,就是一個序列佇列。不過我的觀點是,此時可能執行方式上與序列佇列相似,但是並不能保證就是一個序列佇列,所以也有觀點認為在NSOperationQueue中如果要實現序列佇列的執行方式的話,為每個operation新增執行緒依賴的話會更好。

雖然NSOperationQueue是一個併發佇列,但是我們可以通過修改操作物件的優先順序。優先順序的屬性有下面五個值。比方說operation.queuePriority = NSOperationQueuePriorityHigh;

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
	NSOperationQueuePriorityVeryLow = -8L,
	NSOperationQueuePriorityLow = -4L,
	NSOperationQueuePriorityNormal = 0,
	NSOperationQueuePriorityHigh = 4,
	NSOperationQueuePriorityVeryHigh = 8
};
複製程式碼

GCD(Grand Central Dispatch)

最後一種常用的執行緒方法,就是GCD。作為抽象度最高的技術,使用起來就是簡單和方便。集合了能夠在系統層面提升應用的執行效率,優化體驗等優點於一身,是Apple推薦使用的多執行緒技術(畢竟是親兒子)。

GCD以block為基本操作單元,是一個FIFO佇列,稱為dispatch queue。

不過簡單是簡單,比NSOperation的話,就輸在不能取消操作等缺點上。所以先下結論:如果對執行緒的操作沒什麼特別要求的話,就簡單用GCD就好了,如果對執行緒的操作要求比較高,各種狀態監聽,操作取消的話,就用NSOperation

GCD的各個函式中,末尾帶_f的,比方說dispatch_sync_fdispatch_async_f都是需要傳入系統定義的函式。所以下面的筆記中先忽略帶_f的,因為跟不帶的差不多。。

佇列型別

  1. 主佇列(Serial dispatch queue): 通過dispatch_get_main_queue()獲取程式主佇列。該佇列由系統建立並與主執行緒聯絡在一起。通過dispatch_main(), UIApplicationMain或者是主執行緒的CFRunLoopRef來呼叫提交給主佇列的block

  2. 並行佇列:通過dispatch_get_global_queue(identifier, flags)獲取的佇列。其中需要傳入兩個引數,第一個為佇列的優先順序,第二個為保留引數,傳入0即可。

    優先順序的引數有下面幾種:

    • QOS_CLASS_USER_INTERACTIVE: 表示任務需要立即被執行。通常用於響應使用者輸入和UI更新
    • QOS_CLASS_USER_INITIATED: 表示UI發起的非同步執行。通常用於需要即時結果同時又能繼續響應互動的場景
    • QOS_CLASS_UTILITY: 用於長時間執行的任務,比方說網路資料載入,大資料計算等
    • QOS_CLASS_BACKGROUND: 在後臺執行的任務,不會有使用者感知
    • QOS_CLASS_DEFAULT: 預設優先順序
    • QOS_CLASS_UNSPECIFIED: 未指定
  3. 序列佇列:一般為使用者通過dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)所建立的佇列。其中第一個引數為佇列名,第二個參數列明佇列的型別,當attr為NULL或者是DISPATCH_QUEUE_SERIAL時才是序列佇列,為DISPATCH_QUEUE_CONCURRENT時是並行佇列。

** 對主佇列和全域性並行佇列傳送dispatch_suspend, dispatch_resumedispatch_set_context的話並不會有任何效果**

到了這裡,就得講講佇列的同步和非同步操作,先下demo:

  • 序列佇列,同步執行:

    dispatch_queue_t queue = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
    	NSLog(@"Block 1: %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
    	NSLog(@"Block 2: %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
    	NSLog(@"Block 3: %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
    	NSLog(@"Block 4: %@", [NSThread currentThread]);
    });
    複製程式碼

    輸出結果:

    2017-02-21 22:52:44.896 Thread[37399:1718001] Block 1: <NSThread: 0x6000000667c0>{number = 1, name = main}
    2017-02-21 22:52:44.897 Thread[37399:1718001] Block 2: <NSThread: 0x6000000667c0>{number = 1, name = main}
    2017-02-21 22:52:44.897 Thread[37399:1718001] Block 3: <NSThread: 0x6000000667c0>{number = 1, name = main}
    2017-02-21 22:52:44.898 Thread[37399:1718001] Block 4: <NSThread: 0x6000000667c0>{number = 1, name = main}
    複製程式碼
  • 序列佇列,非同步執行:

    dispatch_queue_t queue = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
    	NSLog(@"Block 1: %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
    	NSLog(@"Block 2: %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
    	NSLog(@"Block 3: %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
    	NSLog(@"Block 4: %@", [NSThread currentThread]);
    });
    複製程式碼

    輸出結果:

    2017-02-21 22:57:31.561 Thread[37508:1722281] Block 1: <NSThread: 0x608000264e00>{number = 3, name = (null)}
    2017-02-21 22:57:31.562 Thread[37508:1722281] Block 2: <NSThread: 0x608000264e00>{number = 3, name = (null)}
    2017-02-21 22:57:31.562 Thread[37508:1722281] Block 3: <NSThread: 0x608000264e00>{number = 3, name = (null)}
    2017-02-21 22:57:31.562 Thread[37508:1722281] Block 4: <NSThread: 0x608000264e00>{number = 3, name = (null)}
    複製程式碼
  • 並行佇列,同步執行

    dispatch_queue_t queue = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(queue, ^{
    	NSLog(@"Block 1: %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
    	NSLog(@"Block 2: %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
    	NSLog(@"Block 3: %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
    	NSLog(@"Block 4: %@", [NSThread currentThread]);
    });
    複製程式碼

    輸出結果:

    2017-02-21 22:52:44.896 Thread[37399:1718001] Block 1: <NSThread: 0x6000000667c0>{number = 1, name = main}
    2017-02-21 22:52:44.897 Thread[37399:1718001] Block 2: <NSThread: 0x6000000667c0>{number = 1, name = main}
    2017-02-21 22:52:44.897 Thread[37399:1718001] Block 3: <NSThread: 0x6000000667c0>{number = 1, name = main}
    2017-02-21 22:52:44.898 Thread[37399:1718001] Block 4: <NSThread: 0x6000000667c0>{number = 1, name = main}
    複製程式碼
  • 並行佇列,非同步執行

    dispatch_queue_t queue = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
    	NSLog(@"Block 1: %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
    	NSLog(@"Block 2: %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
    	NSLog(@"Block 3: %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
    	NSLog(@"Block 4: %@", [NSThread currentThread]);
    });
    複製程式碼

    輸出結果:

    2017-02-21 23:02:29.373 Thread[37623:1726500] Block 2: <NSThread: 0x60000006e780>{number = 4, name = (null)}
    2017-02-21 23:02:29.373 Thread[37623:1726501] Block 1: <NSThread: 0x60000006e700>{number = 3, name = (null)}
    2017-02-21 23:02:29.373 Thread[37623:1726503] Block 3: <NSThread: 0x60000006e900>{number = 5, name = (null)}
    2017-02-21 23:02:29.373 Thread[37623:1726528] Block 4: <NSThread: 0x60000006e980>{number = 6, name = (null)}
    複製程式碼

從上面的程式碼和執行結果可以看出,佇列的型別決定了是否會新開執行緒執行給定的任務,執行的方式決定了執行的順序

佇列型別\執行方式 同步(Synchronous) 非同步(Asynchronous)
序列(Serial) 不會新開執行緒,block按新增的順序執行 建立新的執行緒,block按新增的順序執行(至少現在看來一個dispatch_queue_t物件就會建一個執行緒)
並行(Concurrent) 不會新建執行緒,block按新增的順序執行 根據block的數量建立執行緒,block的執行順序不確定

至少現在看來,序列並行決定了block的執行順序(注意並行同步),同步和非同步決定了執行緒的建立與否

GCD Qos

GCD在後面推出了Qos這個類用來替代原有的優先順序佇列:

Global queue Qos Class
Main thread User-interactive
DISPATCH_QUEUE_PRIORITY_HIGH User-initated
DISPATCH_QUEUE_PRIORITY_DEFAULT Default
DISPATCH_QUEUE_PRIORITY_LOW Utility
DISPATCH_QUEUE_PRIORITY_BACKGROUND Background

dispatch_set_target_queue

dispatch_set_target_queue有兩個作用:

  • 設定優先順序

    使用dispatch_queue_create建立的佇列,如果沒指定優先順序別的話,預設的優先順序是QOS_CLASS_DEFAULT。下面的程式碼是指定了優先順序的例子:

    dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, 0);
    dispatch_queue_t queue = dispatch_queue_create("com", attr);
    複製程式碼

    我們可以通過dispatch_set_target_queue來設定優先順序:

    dispatch_queue_t queue = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t backgroundQueue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0);
    
    
    dispatch_set_target_queue(queue, backgroundQueue);
    // 此時queue的優先順序就跟backgroundQueue一致
    // 也可以像下面這樣
    // dispatch_set_target_queue(queue, DISPATCH_PRIORITY_BACKGROUND);
    複製程式碼
  • 設定佇列層次

    在實際程式中,有可能一個任務會被分割成多個小任務進行執行,但是我們仍需同步執行的話,也可以使用dispatch_set_target_queue來進行設定:建立一個序列佇列,然後將所有任務佇列的參考設為該序列佇列:

    dispatch_queue_t serialQueue = dispatch_queue_create("com.thread.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_SERIAL);
    
    dispatch_set_target_queue(queue, serialQueue);
    dispatch_set_target_queue(queue2, serialQueue);
    複製程式碼

    輸出結果:

    2017-02-22 12:29:21.450 Thread[40387:1879701] Queue 1 Block 1: <NSThread: 0x60800006fa40>{number = 3, name = (null)}
    2017-02-22 12:29:21.450 Thread[40387:1879701] Queue 1 Block 2: <NSThread: 0x60800006fa40>{number = 3, name = (null)}
    2017-02-22 12:29:21.451 Thread[40387:1879701] Queue 2 Block 3: <NSThread: 0x60800006fa40>{number = 3, name = (null)}
    2017-02-22 12:29:21.451 Thread[40387:1879701] Queue 2 Block 4: <NSThread: 0x60800006fa40>{number = 3, name = (null)}
    複製程式碼

dispatch_after

這個函式常用於延遲執行。比方說,我們常用的延遲執行perfromSelector:withObject:afterDelay:

[self performSelector:@selector(test) withObject:nil afterDelay:1];
複製程式碼

如果使用GCD的話,則是:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
	[self test];
});
複製程式碼

這兩者執行起來是一樣的。

dispatch_after會在指定的時間之後,非同步將block新增到指定的佇列中執行

dispatch_once

dispatch_once這個函式我們通常是用於建立單例,因為它的block引數只會在app的宣告週期裡面執行一次。如果同時被多個執行緒所呼叫的話,可能會引起執行緒阻塞

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
	// code to be executed once
});
複製程式碼

訊號量(Semaphore)

GCD中訊號量一般是用來控制執行緒同步的操作,也有用來加鎖解鎖的操作,舉個例子:

dispatch_queue_t queue = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(queue, ^{
	NSLog(@"Hello");
	dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"World");
複製程式碼

上面的這段程式碼,會列印出"Hello"和"World",但是如果註釋掉dispatch_semaphore_signal(semaphore)的話,你就會發現只列印了"Hello"。因為初始化的訊號量為0,呼叫一次dispatch_semaphore_signal()方法的話,會使訊號量+1,當訊號量>0的話,dispatch_semaphore_wait()方法後面的程式碼才會開始執行。就是說訊號量=0時,原來的執行緒會被阻塞,直到“收到”signal,才會繼續執行。

dispatch_barrier

dispatch_barrier常用於並行佇列中。在並行佇列中,可能會出現某個操作我們希望能進行序列執行來避免資源競爭等問題的話,那麼dispatch_barrier就能派上用場了。其保證了在這個並行佇列中,同一時刻只能有一個block在執行,訪問資源。

dispatch_queue_t queue = dispatch_queue_create("com.thread.demo", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
	NSLog(@"1 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
	NSLog(@"2 %@", [NSThread currentThread]);
});

dispatch_barrier_async(queue, ^{
	NSLog(@"Barrier %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
	NSLog(@"3 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
	NSLog(@"4 %@", [NSThread currentThread]);
});
複製程式碼

輸出結果:

2017-02-22 15:16:33.818 Thread[42174:1964999] 2 <NSThread: 0x60800006f9c0>{number = 4, name = (null)}
2017-02-22 15:16:33.819 Thread[42174:1964997] 1 <NSThread: 0x60800006f980>{number = 3, name = (null)}
2017-02-22 15:16:33.822 Thread[42174:1964997] Barrier <NSThread: 0x60800006f980>{number = 3, name = (null)}
2017-02-22 15:16:33.823 Thread[42174:1964997] 3 <NSThread: 0x60800006f980>{number = 3, name = (null)}
2017-02-22 15:16:33.823 Thread[42174:1964999] 4 <NSThread: 0x60800006f9c0>{number = 4, name = (null)}
複製程式碼

總結

囉囉嗦嗦寫了一堆iOS多執行緒相關的知識點,希望自己能夠活學活用吧。不會說學了就忘了。

請大家指正。


參考連結

  1. 深入理解Runloop
  2. Which is the best of GCD, NSThread or NSOperationQueue?
  3. Concurrenty Programming Guide
  4. 細說GCD(Grand Central Dispatch)如何用
  5. Prioritize Work with Quality of Service Classes

相關文章