iOS底層原理 多執行緒之GCD 看我就夠了 --(10)

fgyong發表於2019-07-25

上篇RunLoop已經講過了RunLoop和執行緒的關係,以及Thread如何保活和控制生命週期,今天我們再探究下另外的一個執行緒GCD,揭開蒙娜麗莎的面紗。

GCD 基礎知識

GCD是什麼呢?我們引用百度百科的一段話。

Grand Central Dispatch (GCD)是Apple開發的一個多核程式設計的較新的解決方法。它主要用於優化應用程式以支援多核處理器以及其他對稱多處理系統。它是一個線上程池模式的基礎上執行的並行任務。在Mac OS X 10.6雪豹中首次推出,也可在IOS 4及以上版本使用。

GCD有哪些優點

  • GCD自動管理執行緒
  • 開發者只需要將task加入到佇列中,不用關注細節,然後將task執行完的block傳入即可
  • GCD 自動管理執行緒,執行緒建立,掛起,銷燬。

那麼我們研究下如何更好的使用GCD,首先要了解到序列佇列、並行佇列、併發

序列佇列

序列是基於佇列的,佇列會自己控制執行緒,在序列佇列中,任務一次只能執行一個,執行完當前任務才能繼續執行下個任務。

並行佇列

並行有通過新建執行緒來實現併發執行任務,並行佇列中同時是可能執行多個任務,當並行數量沒有限制的時候,理論上所有任務可以同時執行。

併發

併發是基於執行緒的,同一個執行緒只能序列(同一時刻)執行,要想實現併發,只能多個執行緒一起幹活

序列佇列相當於工廠1條流水線4個工人生產裝置,從開始到結束,一個人只能幹一件事,甲做A不做B。

並行佇列是一條流水線4個工人,當工人幹活速度不夠的時候可以再申請一條流水線,實現兩條流水線同時幹活,這就實現了併發。

併發是多個流水線在同時加工產品。

GCD中的序列佇列()

序列佇列(Serial Dispatch Queue):

按照FIFO(First In First Out先進先出)原則,先新增的任務在隊首,後新增的任務在隊尾,執行任務的時候按照佇列的從首到尾一個挨著一個執行,一次只能執行一個任務,不具備開闢新執行緒的能力。

iOS底層原理 多執行緒之GCD 看我就夠了 --(10)

併發佇列(Concurrent Dispatch Queue):

按照FIFO(First In First Out先進先出)原則,先新增的任務在隊首,後新增的任務在隊尾,執行任務的時候按照佇列的從首到若干個,執行到隊尾,一次可以執行多個任務,具備開闢新執行緒的能力。

iOS底層原理 多執行緒之GCD 看我就夠了 --(10)

GCD使用步驟

GCD的使用非常簡單,建立佇列或者在全域性佇列中新加任務就可以了。

下邊來看看 佇列的建立方法/獲取方法,以及 任務的建立方法

獲取主佇列

主佇列是一種特殊的佇列,也是序列佇列,負責UI的更新,也可以做其他事情,可以通過dispatch_get_main_queue(),一般寫的程式碼沒有宣告多執行緒或者新增到其他佇列中的程式碼都是在主佇列中執行的。

//獲取主佇列
dispatch_queue_t main_queue= dispatch_get_main_queue();
複製程式碼

獲取全域性佇列

全域性佇列是一個特殊的並行佇列,系統已經建立好了,使用的時候通過dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),第一個引數是identifier,表示佇列的優先順序,一般傳入DISPATCH_QUEUE_PRIORITY_DEFAULT,第二個引數flags,官方說法是必須是0,否則返回NULL。暫且傳入0。下邊摘自libdispatch

Use the .Fn dispatch_get_global_queue function to obtain the global queue of given priority. The .Fa flags argument is reserved for future use and must be zero. Passing any value other than zero may result in a NULL return value.

//獲取全域性佇列
dispatch_queue_t main_queue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
複製程式碼

任務的建立

GCD 提供了同步執行任務的建立方法dispatch_sync和非同步執行任務建立方法的spatch_async

// 同步執行任務建立方法
dispatch_sync(queue, ^{
    // 這裡放同步執行任務程式碼
});
// 非同步執行任務建立方法
dispatch_async(queue, ^{
    // 這裡放非同步執行任務程式碼
});
複製程式碼

雖然是隻有同步非同步但是他們組合的多變的

併發佇列 建立的序列佇列 主佇列
同步(sync) 沒開啟新執行緒,序列執行 沒開啟新執行緒,序列執行任務 沒開啟新執行緒,序列執行任務
非同步(async) 能開啟新執行緒,併發執行 能開啟新執行緒,序列執行任務 沒開啟新執行緒,序列執行任務

GCD的使用

主佇列+同步

在主佇列中執行任務,並同步新增任務

//主佇列+同步
-(void)syn_main{
	NSLog(@"1");
	dispatch_queue_t main_queue = dispatch_get_main_queue();
	dispatch_sync(main_queue, ^{
		NSLog(@"2");
	});
	NSLog(@"3");
}
//log
1
複製程式碼

看到日誌只輸出了1就崩潰了提示exc_bad_instuction,為什麼出問題呢? 主佇列是同步的,任務前後執行的任務是在主佇列中,新增的任務也是在主佇列中,而且新增是同步新增。 what???在同步佇列中新增同步任務,到底是想讓佇列執行任務還是新增任務。佇列遵循FIFO原則,假如要大家都在排隊等打飯,新來的員工叫的A,後邊程式碼叫B,然後都在一個佇列中,突然來了個插隊的,你說B能同意嗎?明顯和A幹起來了,結果系統老師過來拉架了說了一句exc_bad_instuction,意思是你倆吵起來大家都吃不上飯了,結果他倆還是接著吵,把系統吵崩潰了。 那麼我們能在主佇列中同步新增任務嗎?答案是可以的。看到答案不要笑哦

//主佇列+同步
-(void)syn_main2{
	NSLog(@"1任務執行");
	sleep(1);
	NSLog(@"2任務執行");
	sleep(1);
	NSLog(@"3任務執行");
}
//log
1任務執行
2任務執行
3任務執行
複製程式碼

沒看錯,保證在主佇列中呼叫該函式,那麼他就是主佇列同步執行的,如果在其他佇列中呼叫,那它則是在呼叫者佇列中同步執行。

主佇列+非同步

在主佇列中非同步新增任務並執行任務

//主佇列+非同步
	NSLog(@"start");
	dispatch_queue_t main_queue = dispatch_get_main_queue();
	dispatch_async(main_queue, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			NSLog(@"%@ %d",[NSThread currentThread],i);
		}
	});
	dispatch_async(main_queue, ^{
		for (int i = 3; i < 6; i ++) {
			[NSThread sleepForTimeInterval:1];
			NSLog(@"%@ %d",[NSThread currentThread],i);
		}
	});
	dispatch_async(main_queue, ^{
		for (int i = 7; i < 10; i ++) {
			[NSThread sleepForTimeInterval:1];
			NSLog(@"%@ %d",[NSThread currentThread],i);
		}
	});
	NSLog(@"end");
//log
2019-07-24 15:12:24.73 start
2019-07-24 15:12:24.73 end

<NSThread: 0x600002f9a940>{number = 1, name = main} 0
2019-07-24 15:18:14.971795+0800 day15-GCD[31837:35880409] <NSThread: 0x600002f9a940>{number = 1, name = main} 1
2019-07-24 15:18:15.972421+0800 day15-GCDo[31837:35880409] <NSThread: 0x600002f9a940>{number = 1, name = main} 2
2019-07-24 15:18:16.973529+0800 day15-GCD[31837:35880409] <NSThread: 0x600002f9a940>{number = 1, name = main} 3
2019-07-24 15:18:17.974978+0800 day15-GCD[31837:35880409] <NSThread: 0x600002f9a940>{number = 1, name = main} 4
2019-07-24 15:18:18.975800+0800 day15-GCD[31837:35880409] <NSThread: 0x600002f9a940>{number = 1, name = main} 5
2019-07-24 15:18:19.977185+0800 day15-GCD[31837:35880409] <NSThread: 0x600002f9a940>{number = 1, name = main} 7
2019-07-24 15:18:20.978615+0800 day15-GCD[31837:35880409] <NSThread: 0x600002f9a940>{number = 1, name = main} 8
2019-07-24 15:18:21.979958+0800 day15-GCD[31837:35880409] <NSThread: 0x600002f9a940>{number = 1, name = main} 9
複製程式碼

在主佇列非同步執行任務,從日誌看出來end早於任務的執行,符合FIFO原則,都是在主執行緒執行,可以看到

  • 主執行緒多個任務非同步不能建立新執行緒
  • 主執行緒非同步也是序列執行

全域性佇列+同步

全域性佇列是並行佇列,和同步配合就是序列執行了。

//全域性佇列+同步
-(void)sync_global{
	printf("\n start");
	dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_sync(global_queue, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_sync(global_queue, ^{
		for (int i = 3; i < 6; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_sync(global_queue, ^{
		NSThread *thread = [NSThread currentThread];
		for (int i = 7; i < 10; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,thread.description.UTF8String,i);
		}
	});
	printf("\n end");
}
//log
start
 2019-07-24 15:35:36 <NSThread: 0x600000592900>{number = 1, name = main} 0
 2019-07-24 15:35:37 <NSThread: 0x600000592900>{number = 1, name = main} 1
 2019-07-24 15:35:38 <NSThread: 0x600000592900>{number = 1, name = main} 2
 2019-07-24 15:35:39 <NSThread: 0x600000592900>{number = 1, name = main} 3
 2019-07-24 15:35:40 <NSThread: 0x600000592900>{number = 1, name = main} 4
 2019-07-24 15:35:41 <NSThread: 0x600000592900>{number = 1, name = main} 5
 2019-07-24 15:35:42 <NSThread: 0x600000592900>{number = 1, name = main} 7
 2019-07-24 15:35:43 <NSThread: 0x600000592900>{number = 1, name = main} 8
 2019-07-24 15:35:44 <NSThread: 0x600000592900>{number = 1, name = main} 9
 end
複製程式碼

在全域性佇列中使用序列新增多個任務並沒有新建子執行緒來解決問題,同步其實就是序列,使用FIFO原則,一個任務解決完再解決下一個任務。

全域性佇列+非同步

全域性佇列有建立子執行緒的能力,但是需要非同步async去執行。

//全域性佇列+非同步
-(void)async_global{
	printf("\n start");
	dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_async(global_queue, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_async(global_queue, ^{
		for (int i = 3; i < 6; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_async(global_queue, ^{
		NSThread *thread = [NSThread currentThread];
		for (int i = 7; i < 10; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,thread.description.UTF8String,i);
		}
	});
	printf("\n end");
}
-(NSString *)currentDateString{
	NSDate *date=[NSDate new];
	NSDateFormatter *format = [[NSDateFormatter alloc]init];
	[format setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
	return [format stringFromDate:date];
}
//log

 start
 end
 2019-07-24 15:40:21 <NSThread: 0x600003b43dc0>{number = 5, name = (null)} 3
 2019-07-24 15:40:21 <NSThread: 0x600003b44e80>{number = 4, name = (null)} 0
 2019-07-24 15:40:21 <NSThread: 0x600003b45880>{number = 3, name = (null)} 7
 2019-07-24 15:40:22 <NSThread: 0x600003b44e80>{number = 4, name = (null)} 1
 2019-07-24 15:40:22 <NSThread: 0x600003b45880>{number = 3, name = (null)} 8
 2019-07-24 15:40:22 <NSThread: 0x600003b43dc0>{number = 5, name = (null)} 4
 2019-07-24 15:40:23 <NSThread: 0x600003b45880>{number = 3, name = (null)} 9
 2019-07-24 15:40:23 <NSThread: 0x600003b44e80>{number = 4, name = (null)} 2
 2019-07-24 15:40:23 <NSThread: 0x600003b43dc0>{number = 5, name = (null)} 5
複製程式碼

全域性佇列當搭配async的時候,追加多個任務,這次是使用3個執行緒,而且不用我們來維護執行緒的生命週期,而且執行的順序是無序的。

建立序列佇列+同步

開發者自己建立的序列佇列同步呼叫和系統主佇列有類似的地方,也有區別。一樣都是序列執行,區別是追加任務的時候一般是在主佇列向序列佇列新增。

//建立序列佇列+同步
-(void)sync_cust_queue{
	printf("\n start");
	dispatch_queue_t custQueue = dispatch_queue_create("cust-queue", DISPATCH_QUEUE_SERIAL);
	dispatch_sync(custQueue, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_sync(custQueue, ^{
		for (int i = 3; i < 6; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_sync(custQueue, ^{
		NSThread *thread = [NSThread currentThread];
		for (int i = 7; i < 10; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,thread.description.UTF8String,i);
		}
	});
	printf("\n end");
}

//log

start
 2019-07-24 15:53:15 <NSThread: 0x6000017ea940>{number = 1, name = main} 0
 2019-07-24 15:53:16 <NSThread: 0x6000017ea940>{number = 1, name = main} 1
 2019-07-24 15:53:17 <NSThread: 0x6000017ea940>{number = 1, name = main} 2
 2019-07-24 15:53:18 <NSThread: 0x6000017ea940>{number = 1, name = main} 3
 2019-07-24 15:53:19 <NSThread: 0x6000017ea940>{number = 1, name = main} 4
 2019-07-24 15:53:20 <NSThread: 0x6000017ea940>{number = 1, name = main} 5
 2019-07-24 15:53:21 <NSThread: 0x6000017ea940>{number = 1, name = main} 7
 2019-07-24 15:53:22 <NSThread: 0x6000017ea940>{number = 1, name = main} 8
 2019-07-24 15:53:23 <NSThread: 0x6000017ea940>{number = 1, name = main} 9
 end
複製程式碼

同步向序列佇列新增任務並沒有死鎖!原因是新增任務是在main_queue執行的,新增的任務是在cust-queue中執行,符合FIFO原則,先新增的先執行,具體執行的執行緒由他們自己分配。執行的任務是在main執行緒中。

建立序列佇列+非同步

會開啟新執行緒,但是因為任務是序列的,執行完一個任務,再執行下一個任務

//建立序列佇列+非同步
-(void)async_cust_queue{
	printf("\n start");
	dispatch_queue_t custQueue = dispatch_queue_create("cust-queue", DISPATCH_QUEUE_SERIAL);
	dispatch_async(custQueue, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_async(custQueue, ^{
		for (int i = 3; i < 6; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_async(custQueue, ^{
		NSThread *thread = [NSThread currentThread];
		for (int i = 7; i < 10; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,thread.description.UTF8String,i);
		}
	});
	printf("\n end");
}
//log

 start
 end
 2019-07-24 16:12:57 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 0
 2019-07-24 16:12:58 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 1
 2019-07-24 16:12:59 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 2
 2019-07-24 16:13:00 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 3
 2019-07-24 16:13:01 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 4
 2019-07-24 16:13:02 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 5
 2019-07-24 16:13:03 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 7
 2019-07-24 16:13:04 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 8
 2019-07-24 16:13:05 <NSThread: 0x600002b346c0>{number = 3, name = (null)} 9
複製程式碼

非同步 + 序列佇列可以看到:

開啟了一條新執行緒(非同步執行具備開啟新執行緒的能力,序列佇列只開啟一個執行緒)。 所有任務是在列印的end之後才開始執行的(非同步執行不會做任何等待,可以繼續執行任務)。 任務是按順序執行的(序列佇列每次只有一個任務被執行,任務一個接一個按順序執行)。

建立並行佇列+同步

在當前執行緒中執行任務,不會開啟新執行緒,執行完一個任務,再執行下一個任務

 start
 2019-07-24 16:21:24 <NSThread: 0x6000031d1380>{number = 1, name = main} 0
 2019-07-24 16:21:25 <NSThread: 0x6000031d1380>{number = 1, name = main} 1
 2019-07-24 16:21:26 <NSThread: 0x6000031d1380>{number = 1, name = main} 2
 2019-07-24 16:21:27 <NSThread: 0x6000031d1380>{number = 1, name = main} 3
 2019-07-24 16:21:28 <NSThread: 0x6000031d1380>{number = 1, name = main} 4
 2019-07-24 16:21:29 <NSThread: 0x6000031d1380>{number = 1, name = main} 5
 2019-07-24 16:21:30 <NSThread: 0x6000031d1380>{number = 1, name = main} 7
 2019-07-24 16:21:31 <NSThread: 0x6000031d1380>{number = 1, name = main} 8
 2019-07-24 16:21:32 <NSThread: 0x6000031d1380>{number = 1, name = main} 9
 end
複製程式碼

全域性佇列其實就是特殊的並行佇列,這裡結果和全域性佇列+同步一致。

建立並行佇列+非同步

在當前執行緒中執行任務,會開啟新執行緒,可以同時執行多個任務。

//建立並行佇列+非同步
-(void)async_queue{
	printf("\n start");
	dispatch_queue_t custQueue = dispatch_queue_create("cust-queue", DISPATCH_QUEUE_CONCURRENT);
	dispatch_async(custQueue, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_async(custQueue, ^{
		for (int i = 3; i < 6; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
		}
	});
	dispatch_async(custQueue, ^{
		NSThread *thread = [NSThread currentThread];
		for (int i = 7; i < 10; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self currentDateString].UTF8String,thread.description.UTF8String,i);
		}
	});
	printf("\n end");
}
//log
start
 end
 2019-07-24 16:22:09 <NSThread: 0x6000004280c0>{number = 3, name = (null)} 7
 2019-07-24 16:22:09 <NSThread: 0x6000004104c0>{number = 5, name = (null)} 0
 2019-07-24 16:22:09 <NSThread: 0x600000422300>{number = 4, name = (null)} 3
 2019-07-24 16:22:10 <NSThread: 0x6000004104c0>{number = 5, name = (null)} 1
 2019-07-24 16:22:10 <NSThread: 0x6000004280c0>{number = 3, name = (null)} 8
 2019-07-24 16:22:10 <NSThread: 0x600000422300>{number = 4, name = (null)} 4
 2019-07-24 16:22:11 <NSThread: 0x6000004280c0>{number = 3, name = (null)} 9
 2019-07-24 16:22:11 <NSThread: 0x6000004104c0>{number = 5, name = (null)} 2
 2019-07-24 16:22:11 <NSThread: 0x600000422300>{number = 4, name = (null)} 5
複製程式碼

並行佇列+非同步全域性佇列+非同步一致,也會新建執行緒執行任務,且是併發執行。

GCD其他高階用法

子執行緒執行任務 主執行緒重新整理UI

- (void)backToMain{
	dispatch_queue_t main = dispatch_get_main_queue();
	dispatch_queue_t glo = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
	dispatch_async(glo, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self dateUTF8],[self threadInfo],i);
		}
		dispatch_sync(main, ^{
			printf("\n %s %s 我在重新整理UI",[self dateUTF8],[self threadInfo]);
		});
	});	
}
//log
 2019-07-24 16:45:07 <NSThread: 0x600001e84380>{number = 3, name = (null)} 0
 2019-07-24 16:45:08 <NSThread: 0x600001e84380>{number = 3, name = (null)} 1
 2019-07-24 16:45:09 <NSThread: 0x600001e84380>{number = 3, name = (null)} 2
 2019-07-24 16:45:09 <NSThread: 0x600001ef2940>{number = 1, name = main} 我在重新整理UI
複製程式碼

佇列分組 dispatch_group_t

dispatch_group_notify

GCD有有分組的概念,當所有加入分組的佇列中的任務都執行完成的時候,通過dispatch_group_notify完成回撥,第一個引數group是某個分組的回撥。

-(void)group{
	dispatch_group_t group = dispatch_group_create();
	dispatch_queue_t queue= dispatch_queue_create("cust.queue.com", DISPATCH_QUEUE_CONCURRENT);
	dispatch_queue_t queue2= dispatch_queue_create("cust2.queue.com", DISPATCH_QUEUE_CONCURRENT);
	dispatch_group_async(group, queue, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self dateUTF8],[self threadInfo],i);
		}
	});
	dispatch_group_async(group, queue2, ^{
		for (int i = 4; i < 6; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self dateUTF8],[self threadInfo],i);
		}
	});
	dispatch_group_notify(group, dispatch_get_main_queue(), ^{
		printf("\n %s %s ---end1----",[self dateUTF8],[self threadInfo]);
	});
	dispatch_group_async(group, queue, ^{
		for (int i = 6; i < 8; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self dateUTF8],[self threadInfo],i);
		}
	});
	dispatch_group_async(group, queue2, ^{
		for (int i = 8; i < 10; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self dateUTF8],[self threadInfo],i);
		}
	});
}
複製程式碼
dispatch_group_wait && dispatch_group_enter && dispatch_group_leave

dispatch_group_enterdispatch_group_leave需要成對使用,否則dispatch_group_wait在缺少leave的情況下會等待到死,造成執行緒阻塞。

static	dispatch_group_t group ;
if (group == nil) {
	group = dispatch_group_create();
}
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//	dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
	[self print];
	[NSThread sleepForTimeInterval:2];
//		dispatch_group_leave(group);//當註釋掉  阻塞在wait不繼續向下執行
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
	[self print];
	[NSThread sleepForTimeInterval:2];
	dispatch_group_leave(group);
});
//log
2019-07-25 10:58:50 <NSThread: 0x600002d84180>{number = 3, name = (null)} 
2019-07-25 10:58:52 <NSThread: 0x600002d84180>{number = 3, name = (null)} 

複製程式碼

柵欄函式 dispatch_barrier_sync

柵欄函式實現了非同步的佇列中在多個任務結束的時候實行回撥,回撥分非同步和同步,同步回撥在主執行緒,非同步在其他執行緒。

- (void)barry{
	dispatch_queue_t queue= dispatch_queue_create("cust.queue.com", DISPATCH_QUEUE_CONCURRENT);
	dispatch_async(queue, ^{
		for (int i = 0; i < 3; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self dateUTF8],[self threadInfo],i);
		}
	});
	dispatch_barrier_sync(queue, ^{
		printf("\n %s %s ---中間暫停一下----",[self dateUTF8],[self threadInfo]);
	});
	dispatch_async(queue, ^{
		for (int i = 3; i < 6; i ++) {
			[NSThread sleepForTimeInterval:1];
			printf("\n %s %s %d",[self dateUTF8],[self threadInfo],i);
		}
	});
	dispatch_barrier_async(queue, ^{
		printf("\n %s %s ---中間第二次暫停一下----",[self dateUTF8],[self threadInfo]);
	});
}
//log
 2019-07-24 16:52:33 <NSThread: 0x600003158440>{number = 3, name = (null)} 0
 2019-07-24 16:52:34 <NSThread: 0x600003158440>{number = 3, name = (null)} 1
 2019-07-24 16:52:35 <NSThread: 0x600003158440>{number = 3, name = (null)} 2
 2019-07-24 16:52:35 <NSThread: 0x6000031293c0>{number = 1, name = main} ---中間暫停一下----
 2019-07-24 16:52:36 <NSThread: 0x600003158440>{number = 3, name = (null)} 3
 2019-07-24 16:52:37 <NSThread: 0x600003158440>{number = 3, name = (null)} 4
 2019-07-24 16:52:38 <NSThread: 0x600003158440>{number = 3, name = (null)} 5
 2019-07-24 16:52:38 <NSThread: 0x600003158440>{number = 3, name = (null)} ---中間第二次暫停一下----
複製程式碼

單例-執行一次的函式 dispatch_once_t

單例可以通過這個函式實現,只執行一次的函式。

//只執行一次的dispatch_once
-(void)exc_once{
	static dispatch_once_t onceToken;
	static NSObject *obj;
	dispatch_once(&onceToken, ^{
		obj=[NSObject new];
		printf("\n just once %s %s",[self dateUTF8],obj.description.UTF8String);
	});
	printf("\n %s %s",[self dateUTF8],obj.description.UTF8String);
}
呼叫4次
dispatch_apply(4, dispatch_get_global_queue(0, 0), ^(size_t idx) {
		[self exc_once];
	});
	
//log
just once 2019-07-25 14:46:00 <NSObject: 0x60000378b100>
2019-07-25 14:46:00 <NSObject: 0x60000378b100>
2019-07-25 14:46:00 <NSObject: 0x60000378b100>
2019-07-25 14:46:00 <NSObject: 0x60000378b100>
複製程式碼

當呼叫4次的時候,日誌列印的四次obj均為同一個地址,證明block回撥四次但是隻執行了一次。

延遲執行 dispatch_after

當記錄日誌或者點選事件的方法我們不希望立即執行,則會用到延遲

//延遲執行
-(void)delayTimeExc{
	printf("\n %s %s begin",[self dateUTF8],[self threadInfo]);
	
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
	
		printf("\n %s %s",[self dateUTF8],[self threadInfo]);
		
	});
	printf("\n %s %s end",[self dateUTF8],[self threadInfo]);
}
//log
2019-07-24 17:07:48 <NSThread: 0x600003cc6940>{number = 1, name = main} begin
2019-07-24 17:07:48 <NSThread: 0x600003cc6940>{number = 1, name = main} end
2019-07-24 17:07:50 <NSThread: 0x600003cc6940>{number = 1, name = main}
複製程式碼

訊號量 dispatch_semaphore_t

訊號量為1可以作為執行緒鎖來用,當N>1的時候,同時執行的有N個任務。 dispatch_apply可以通知建立多個執行緒來執行任務,用它來測試訊號量再好不過了。

//訊號量 當訊號量為1 可以未做鎖來用,當N>1,t通知執行的數量則是數字N。
- (void)semaphore{
	static dispatch_semaphore_t sem;
	if (sem == NULL) {
		sem = dispatch_semaphore_create(1);
	}
	dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
	static int i = 0;
	int currentI = i +2;
	for (; i < currentI; i ++) {
		[NSThread sleepForTimeInterval:1];
		printf("\n %s %s %d",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,i);
	}
	dispatch_semaphore_signal(sem);
}
-(void)asyn_semaphore{
	dispatch_apply(3, dispatch_get_global_queue(0, 0), ^(size_t idx) {
		[self semaphore];
	});
}
//log

2019-07-24 17:25:04 <NSThread: 0x6000002a2940>{number = 1, name = main} 0
2019-07-24 17:25:05 <NSThread: 0x6000002a2940>{number = 1, name = main} 1
2019-07-24 17:25:06 <NSThread: 0x6000002e2b40>{number = 3, name = (null)} 2
2019-07-24 17:25:07 <NSThread: 0x6000002e2b40>{number = 3, name = (null)} 3
2019-07-24 17:25:08 <NSThread: 0x6000002d4740>{number = 4, name = (null)} 4
2019-07-24 17:25:09 <NSThread: 0x6000002d4740>{number = 4, name = (null)} 5
複製程式碼

設計一個經典問題,火車票視窗買票,火車站賣票一般有多個視窗,排隊是每個視窗排一個佇列,一個視窗同時只能賣一張票,那我們設計一下如何實現多佇列同時訪問多個視窗的的問題。

-(void)muchQueueBuyTick{
	dispatch_queue_t queue= dispatch_queue_create("com.buy.tick", DISPATCH_QUEUE_CONCURRENT);
	dispatch_async(queue, ^{
		for (NSInteger i = 0; i < 5; i ++) {
			[self semaphore_buy_ticks:4];
		}
	});
	dispatch_queue_t queue2= dispatch_queue_create("com.buy2.tick", DISPATCH_QUEUE_CONCURRENT);
	dispatch_async(queue2, ^{
		for (NSInteger i = 0; i < 5; i ++) {
			[self semaphore_buy_ticks:2];
		}
	});
}
- (void)semaphore_buy_ticks:(NSInteger)windowsCount{
	static dispatch_semaphore_t sem;
	if (sem == NULL) {
		sem = dispatch_semaphore_create(windowsCount);
	}
	//訊號量-1
	dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
	self.count--;
	if (self.count > 0) {
		printf("\n %s %s 第%ld個人買到票了",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,(long)self.count);
		[NSThread sleepForTimeInterval:0.2];
	}
	//訊號量+1
	dispatch_semaphore_signal(sem);
}
//log

2019-07-24 18:01:44 <NSThread: 0x600003935e00>{number = 4, name = (null)} 第8個人買到票了
 2019-07-24 18:01:44 <NSThread: 0x600003904c40>{number = 3, name = (null)} 第8個人買到票了
 2019-07-24 18:01:45 <NSThread: 0x600003904c40>{number = 3, name = (null)} 第6個人買到票了
 2019-07-24 18:01:45 <NSThread: 0x600003935e00>{number = 4, name = (null)} 第6個人買到票了
 2019-07-24 18:01:45 <NSThread: 0x600003935e00>{number = 4, name = (null)} 第4個人買到票了
 2019-07-24 18:01:45 <NSThread: 0x600003904c40>{number = 3, name = (null)} 第4個人買到票了
 2019-07-24 18:01:45 <NSThread: 0x600003935e00>{number = 4, name = (null)} 第2個人買到票了
 2019-07-24 18:01:45 <NSThread: 0x600003904c40>{number = 3, name = (null)} 第2個人買到票了
 2019-07-24 18:01:45 <NSThread: 0x600003935e00>{number = 4, name = (null)} 第0個人買到票了
複製程式碼

兩個視窗(兩個佇列),每個視窗排了5(迴圈5次)個人,一共10(count=10)張票。 當同時一張票可以分割2次,賣票的錯亂了,明顯錯誤了,現在把每張票都鎖起來,同時只能允許同一個人賣。

-(void)muchQueueBuyTick{
	dispatch_queue_t queue= dispatch_queue_create("com.buy.tick", DISPATCH_QUEUE_CONCURRENT);
	dispatch_async(queue, ^{
		for (NSInteger i = 0; i < 5; i ++) {
			[self semaphore_buy_ticks:1];
		}
	});
	dispatch_queue_t queue2= dispatch_queue_create("com.buy2.tick", DISPATCH_QUEUE_CONCURRENT);
	dispatch_async(queue2, ^{
		for (NSInteger i = 0; i < 5; i ++) {
			[self semaphore_buy_ticks:1];
		}
	});
}
//log
2019-07-24 18:03:56 <NSThread: 0x600000e1cac0>{number = 3, name = (null)} 第9個人買到票了
 2019-07-24 18:03:57 <NSThread: 0x600000e1e0c0>{number = 4, name = (null)} 第8個人買到票了
 2019-07-24 18:03:57 <NSThread: 0x600000e1cac0>{number = 3, name = (null)} 第7個人買到票了
 2019-07-24 18:03:57 <NSThread: 0x600000e1e0c0>{number = 4, name = (null)} 第6個人買到票了
 2019-07-24 18:03:57 <NSThread: 0x600000e1cac0>{number = 3, name = (null)} 第5個人買到票了
 2019-07-24 18:03:57 <NSThread: 0x600000e1e0c0>{number = 4, name = (null)} 第4個人買到票了
 2019-07-24 18:03:58 <NSThread: 0x600000e1cac0>{number = 3, name = (null)} 第3個人買到票了
 2019-07-24 18:03:58 <NSThread: 0x600000e1e0c0>{number = 4, name = (null)} 第2個人買到票了
 2019-07-24 18:03:58 <NSThread: 0x600000e1cac0>{number = 3, name = (null)} 第1個人買到票了
複製程式碼

順序是對了,數量也對了。

再換一種思路實現鎖住視窗,我們使用序列佇列也是可以的。

//使用同步佇列賣票
- (void)sync_buy_tick{
	dispatch_async(dispatch_get_main_queue(), ^{
		self.count--;
		if (self.count > 0) {
			printf("\n %s %s 第%ld個人買到票了",[self currentDateString].UTF8String,[NSThread currentThread].description.UTF8String,(long)self.count);
			[NSThread sleepForTimeInterval:0.2];
		}
	});
}
//log
2019-07-25 09:31:56 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第9個人買到票了
 2019-07-25 09:31:56 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第8個人買到票了
 2019-07-25 09:31:56 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第7個人買到票了
 2019-07-25 09:31:56 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第6個人買到票了
 2019-07-25 09:31:57 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第5個人買到票了
 2019-07-25 09:31:57 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第4個人買到票了
 2019-07-25 09:31:57 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第3個人買到票了
 2019-07-25 09:31:57 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第2個人買到票了
 2019-07-25 09:31:57 <NSThread: 0x6000034d2e40>{number = 1, name = main} 第1個人買到票了
複製程式碼

序列佇列不建立子執行緒,所有任務都在同一個執行緒執行,那麼他們就會排隊,其實不管多少人同時點選買票,票的分割還是序列的,所以執行緒鎖的可以使用序列佇列來解決。

快速迭代方法:dispatch_apply

快速迭代就是同時建立很多執行緒來在做事情,現在工廠收到一個億的訂單,工廠本來只有2條生產線,現在緊急新建很多生產線來生產產品。

/*
同時新建了多條執行緒來做任務
*/
	dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t idx) {
		printf("\n %s %s ",[self dateUTF8],[self threadInfo]);
	});
	
	//log
2019-07-25 09:38:38 <NSThread: 0x600000966180>{number = 4, name = (null)} 
2019-07-25 09:38:38 <NSThread: 0x60000094a3c0>{number = 3, name = (null)} 
2019-07-25 09:38:38 <NSThread: 0x600000979dc0>{number = 5, name = (null)} 
2019-07-25 09:38:38 <NSThread: 0x60000090d3c0>{number = 1, name = main} 
2019-07-25 09:38:38 <NSThread: 0x60000095cfc0>{number = 6, name = (null)} 
2019-07-25 09:38:38 <NSThread: 0x600000950140>{number = 8, name = (null)} 
 2019-07-25 09:38:38 <NSThread: 0x60000095d0c0>{number = 9, name = (null)} 
 2019-07-25 09:38:38 <NSThread: 0x60000094a400>{number = 7, name = (null)} 
 2019-07-25 09:38:38 <NSThread: 0x600000966180>{number = 4, name = (null)} 
 2019-07-25 09:38:38 <NSThread: 0x60000094a3c0>{number = 3, name = (null)} 
複製程式碼

可以看到新建了3456789main來執行任務。

多執行緒RunLoop實戰

問題一:請問下邊程式碼輸出什麼?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    dispatch_queue_t  que= dispatch_get_global_queue(0, 0);
    dispatch_async(que, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
    });
}
- (void)test{
    NSLog(@"2");
}
複製程式碼
  • 猜想1:結果是123
  • 猜想2:結果是132

有沒有第三種結果呢?

猜想1分析: 因為是延遲0s執行,當然是先執行2,再執行3了。

猜想2分析:

我們來分析一下,非同步加入全域性佇列中,單個任務的時候會加入到子執行緒中,那麼會先輸出1,然後輸出3,最後輸出2.

最後驗證一下:

1
3
複製程式碼

為什麼2沒有出來呢?在看一下程式碼,全域性佇列,延遲執行,點進去函式檢視,原來是在runloop.h檔案中,我們猜測延遲執行是timer新增到runloop中了,新增進去也應該輸出132的。因為在子執行緒中,沒有主動呼叫不會有runloop的,及時呼叫了也需要保活技術,那麼程式碼改進一下

    dispatch_queue_t  que= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(que, ^{
        NSLog(@"1");
        // 相當於[self test];
//       [self performSelector:@selector(test) withObject:nil];
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"3");
    });
複製程式碼

經測試輸出了12,這和我們猜想的還是不對,原來輸出3放在了最後,導致的問題,RunLoop執行起來,進入了迴圈,則後面的就不會執行了,除非停止當前RunLoop,我們再改進一下

    dispatch_queue_t  que= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(que, ^{
        NSLog(@"1");
        // 相當於[self test];
//       [self performSelector:@selector(test) withObject:nil];
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
         NSLog(@"3");
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    });
複製程式碼

最後終於輸出了132。缺點是子執行緒成了死待,不死之身,關於怎麼殺死死待請看上篇優雅控制RunLoop生命週期。 關於performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay中有延遲的,都是新增到當前你執行緒的RunLoop,如果沒有啟動RunLoop和保活恐怕也不能一直執行。[self performSelector:@selector(test) withObject:nil]是在Foudation中,原始碼是直接objc_msgSend(),相當於直接[self test],不會有延遲。

問題2:請問輸出什麼?

NSThread *thread=[[NSThread alloc]initWithBlock:^{
    NSLog(@"1");
}];
[thread start];
[self performSelector:@selector(test)
             onThread:thread
           withObject:nil
        waitUntilDone:YES];
複製程式碼

這個和上面的類似,結果是列印了1就崩潰了,原因是thread start之後執行完block就結束了,沒有runloop的支撐。當執行performSelector的時候,執行緒已經死掉。解決這個問題只需要向子執行緒中新增RunLoop,而且保證RunLoop不停止就行了。

總結

  • GCD非同步負責執行耗時任務(例如下載,複雜計算),main執行緒負責更新UI
  • 佇列多工非同步執行最後全域性執行完畢可以使用group_notify來監聽執行完畢時間
  • 佇列多工非同步執行結束時間,中間攔截更新UI,然後再非同步執行可以使用dispatch_barrier_sync
  • 當多執行緒訪問同一個資源,可以使用訊號量來限制同時訪問資源的執行緒數量

參考資料

資料下載


最怕一生碌碌無為,還安慰自己平凡可貴。

廣告時間

iOS底層原理 多執行緒之GCD 看我就夠了 --(10)

相關文章