目錄
- 簡述
- NSThread
- GCD
- 操作與隊列
- 異步操作并行隊列
- 同步操作并行隊列
- 同步操作串行隊列
- 異步操作串行隊列
- 隊列創建
- 新建隊列
- 系統提供
- dispatch_set_target_queue
- dispatch_after
- dispatch Group
- dispatch_barrier_async
- 操作與隊列
- NSOperation
- NSOperationQueue
- 添加依賴
- 其他用法
- 優劣比較
- 常見并發問題及其解決方案
- 死鎖
- 數據競爭
- 同步執行
簡述
在學習多線程之前,我們先了解線程是什么。線程通俗的理解就是一條單車道。例如下面這個helloworld
NSString *str=@"Hello, World!";
NSLog(@"%@",str);
這段OC代碼在經過編譯器以后,會轉換成二進制代碼。這些二進制代碼會被機器一條一條執行。由于CPU一次只能執行一個命令(單車道),因此,我們將CPU執行CPU命令列(二進制代碼)的路徑稱之為線程。
光有單車道可不行,那樣會堵死的,我們需要雙車道乃至三四五六七八車道,這就是多線程。無論CPU再怎么厲害,它每次執行的命令只能有一個,就像我道路的寬度已經被規定為一輛小汽車的寬度,那我們在不擴寬道路的情況下要容納更多的汽車,只能再加幾條道路。
OS X和iOS的核心XNU內核在發生系統操作事件時會切換執行路徑。執行中的路徑狀態,例如CPU的寄存器等信息保存到各自路徑專用的內存塊中,從切換目標路徑專用的內存塊中,復原CPU寄存器等信息,繼續執行切換路徑的CPU命令列,這被稱之為“上下文切換”。
多線程的實現原理就是基于線程之間反復多次的上下文切換,下面就是iOS開發中比較常見的多線程技術。
NSThread
NSThread是蘋果提供的一種輕量級的線程方法,因為它的輕量,所以線程的生命周期、同步、加鎖問題都需要自己手動管理。一般像獲取當前線程、讓程序回到主線程更新UI,用NSThread封裝的方法比較方便。
使用方法
創建&開啟
//創建線程,里面包含run方法
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
// 設置線程的優先級(0.0 - 1.0,1.0最高級)
thread.threadPriority = 1;
// 開啟線程
[thread start];
獲取&暫停線程
//獲取當前線程
NSThread *current = [NSThread currentThread];
//獲取主線程
NSThread *main = [NSThread mainThread];
// 暫停2s
[NSThread sleepForTimeInterval:2];
// 或者
NSDate *date = [NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]];
[NSThread sleepUntilDate:date];
線程通信
//在指定線程執行操作
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES];
//在主線程執行操作
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];
//在當前線程執行操作
[self performSelector:@selector(run) withObject:nil];
GCD
什么是GCD,GCD(grand central dispatch)是蘋果提供的異步執行任務技術之一。一般將應用程序中技術的線程管理用的代碼在系統級實現。開發者只需要定義想執行的任務并追加到適當的dispatch queue中,GCD就能生成必要的線程并計劃執行任務。由于線程管理是作為系統的一部分來實現的,因此可統一管理,也可執行任務,這樣就比以前的線程更有效率。另外在性能方面,GCD的線程管理是系統級的,也就是在XNU內核上實現的,因此無論我們再怎么編,再怎么使用pthread(C語言的線程框架)和NSThread,性能都不可能超過GCD,因此如果需要統一線程管理,首選GCD。
操作與隊列
GCD是一個很大的模塊,因此我先從操作與隊列開始講起,只是看使用的話,看這一點也差不多了。在GCD中,有4個容易混淆的概念:同步、異步、并行、串行。
同步(sync) 和 異步(async) 修飾的都是操作,主要區別在于會不會阻塞當前線程,直到 Block 中的任務執行完畢!
- 如果是 同步(sync) 操作,它會阻塞當前線程并等待 Block 中的任務執行完畢,然后當前線程才會繼續往下運行。
- 如果是 異步(async)操作,當前線程會直接往下執行,它不會阻塞當前線程。
并行(concurrent)和串行(serial)修飾的都是隊列,主要區別在于隊列里的任務是否需要等待其他任務。
- 如果是串行(serial)隊列,GCD 會 FIFO(先進先出) 地取出來一個放在隊列里面的任務,執行一個,然后取下一個,這樣一個一個的執行
- 如果并行(concurrent)隊列,則不會等待正在處理中的任務。
我們使用GCD,實際上就是操作(同步、異步)隊列(串行、并行),因此一共有4種組合方式。他們的組合會產生怎樣的效果?見下表
同步 | 異步 | |
---|---|---|
串行 | 當前線程,任務按順序執行 | 其他某個線程,任務按順序執行 |
并行 | 當前線程,任務按順序執行 | 其他某些線程,任務一起執行 |
如果要簡單記憶的話,可以記憶成異步一定會開啟線程,并行只有遇到異步調用的時候才是真正的一起執行。而具體怎么使用看具體情況而定
- 有次序不阻塞線程,創建串行隊列,異步操作
- 開啟多個線程同時執行(不推薦,線程開銷大),創建并行隊列,異步操作。
下面是實現demo
異步操作并行隊列
通過dispatch_queue_create方法,創建了一個名叫"test.mathewchen.queue"的隊列,第二個參數是確定該隊列為并行或者串行,null為串行,DISPATCH_QUEUE_CONCURRENT為并行。
dispatch_queue_t queue = dispatch_queue_create("test.mathewchen.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^(void){
NSLog(@"block:%d,thread:%@",0,[NSThread currentThread]);
});
......
dispatch_async(queue, ^(void){
NSLog(@"block:%d,thread:%@",13,[NSThread currentThread]);
});
打出的結果
2016-09-21 17:05:26.735 testGCD[4732:1575766] block:1,thread:<NSThread: 0x7fcc51605340>{number = 3, name = (null)}
2016-09-21 17:05:26.735 testGCD[4732:1575774] block:2,thread:<NSThread: 0x7fcc5166eb60>{number = 5, name = (null)}
2016-09-21 17:05:26.735 testGCD[4732:1575776] block:0,thread:<NSThread: 0x7fcc51417130>{number = 2, name = (null)}
2016-09-21 17:05:26.735 testGCD[4732:1575784] block:3,thread:<NSThread: 0x7fcc5171beb0>{number = 4, name = (null)}
2016-09-21 17:05:26.735 testGCD[4732:1575785] block:4,thread:<NSThread: 0x7fcc51410180>{number = 6, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575766] block:5,thread:<NSThread: 0x7fcc51605340>{number = 3, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575774] block:6,thread:<NSThread: 0x7fcc5166eb60>{number = 5, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575776] block:7,thread:<NSThread: 0x7fcc51417130>{number = 2, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575784] block:8,thread:<NSThread: 0x7fcc5171beb0>{number = 4, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575785] block:9,thread:<NSThread: 0x7fcc51410180>{number = 6, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575766] block:10,thread:<NSThread: 0x7fcc51605340>{number = 3, name = (null)}
2016-09-21 17:05:26.737 testGCD[4732:1575774] block:11,thread:<NSThread: 0x7fcc5166eb60>{number = 5, name = (null)}
2016-09-21 17:05:26.737 testGCD[4732:1575786] block:12,thread:<NSThread: 0x7fcc51670c50>{number = 7, name = (null)}
2016-09-21 17:05:26.737 testGCD[4732:1575776] block:13,thread:<NSThread: 0x7fcc51417130>{number = 2, name = (null)}
很顯然,當異步調用包含n個任務的并行隊列的話,會創建m條線程,m<=n,具體開幾個線程是根據任務的耗時定的,例如我們上面的例子,一開始先開了35246這五個線程,當執行到第六個block時,系統發現第一個線程已經完事了,復用了一開始創建的3線程,后面執行block12的時候,系統發現沒有線程可以復用(name=2線程的任務沒有執行完畢),于是新開了一個7線程,因為是并行隊列所以前面block012順序有點亂,因此,可以整理出線程分配表如下。
3 | 5 | 2 | 4 | 6 | 7 |
---|---|---|---|---|---|
block1 | block2 | block0 | block3 | block4 | 復用3 |
block5 | block6 | block7 | block8 | block9 | 復用3 |
block10 | block11 | block13 | block12 |
同步操作并行隊列
現在我們看一下同步執行并行隊列會打出什么LOG,代碼如下
dispatch_sync(queue, ^(void){
NSLog(@"block:%d,thread:%@",0,[NSThread currentThread]);
});
打出的log
2016-09-22 08:53:46.173 testGCD[4879:1599485] block:0,thread:<NSThread: 0x7fe761704ac0>{number = 1, name = main}
2016-09-22 08:53:46.173 testGCD[4879:1599485] block:1,thread:<NSThread: 0x7fe761704ac0>{number = 1, name = main}
······
2016-09-22 08:53:46.187 testGCD[4879:1599485] block:13,thread:<NSThread: 0x7fe761704ac0>{number = 1, name = main}
很明顯的看出,并行隊列在同步操作中是失效的。之所以會出現這種情況,是因為GCD操作并行隊列的本質所決定的。GCD在操作隊列時,無論是并行還是串行,都會根據FIFO(先進先出)原則,將隊列中的任務取出。區別在于,串行會放入同一個線程執行任務,而并行取出來一個就會放到別的線程,然后再取出來一個又放到另一個的線程。這樣由于取的動作很快,忽略不計,看起來,所有的任務都是一起執行的。由于同步操作是單線程的,并行隊列沒有其他線程可以執行任務,因此才會產生了在同步操作下,串行和并行隊列沒有區別。
同步操作串行隊列
這個沒什么好說的了,打出的LOG和同步操作并行隊列一模一樣。
異步操作串行行隊列
代碼如下
dispatch_queue_t queue = dispatch_queue_create("test.mathewchen.queue", NULL);
dispatch_async(queue, ^(void){
NSLog(@"block:%d,thread:%@",0,[NSThread currentThread]);
});
......
dispatch_async(queue, ^(void){
NSLog(@"block:%d,thread:%@",13,[NSThread currentThread]);
});
打出的log
2016-09-22 11:21:31.640 testGCD[5058:1633715] block:0,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.641 testGCD[5058:1633715] block:1,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.641 testGCD[5058:1633715] block:2,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.642 testGCD[5058:1633715] block:3,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.643 testGCD[5058:1633715] block:4,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.643 testGCD[5058:1633715] block:5,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.644 testGCD[5058:1633715] block:6,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.644 testGCD[5058:1633715] block:7,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.649 testGCD[5058:1633715] block:8,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.651 testGCD[5058:1633715] block:9,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.667 testGCD[5058:1633715] block:10,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.668 testGCD[5058:1633715] block:11,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.668 testGCD[5058:1633715] block:12,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
2016-09-22 11:21:31.668 testGCD[5058:1633715] block:13,thread:<NSThread: 0x7fc3f0e12240>{number = 2, name = (null)}
可以看到,異步調用串行隊列,為了不阻塞主線程,會新開一個線程。
隊列的創建
在GCD中,獲取隊列的方式有兩種,一種是新建隊列,另一種是使用系統標準提供的隊列。在MRC中隊列使用完成還需要調用dispatch_release釋放掉,而ARC中已經幫我們做了。
新建隊列
在GCD中可以使用如下API進行隊列的創建:
dispatch_queue_t queue = dispatch_queue_create("test.mathewchen.queue", DISPATCH_QUEUE_CONCURRENT);
第一個參數是為我們新建的隊列命名,第二個參數則決定了我們該隊列的性質:并行or串行。第二個參數是DISPATCH_QUEUE_SERIAL或NULL(常用)時,創建的隊列為串行隊列。如果為DISPATCH_QUEUE_CONCURRENT則為并行隊列。
系統提供
即使我們什么也不做,系統也會給我們提供幾個隊列供我們使用。一種是Main Dispatch Queue,另一種則是Global Dispatch Queue。
Main Dispatch Queue就是我們常用的主線程隊列,這是一個串行隊列,我們想要更新UI必須在這個線程執行,可以通過該方法獲取:
dispatch_get_main_queue()
Global Dispatch Queue是所有應用程序都能夠使用的并行隊列,創建隊列是有開銷的,因此一般情況下我們應該使用系統給我們提供的Global Dispatch Queue。
Global Dispatch Queue有四個優先級,分別是High Priority,Default Priority,Low Priority,Background Priority。通過creat創建的所有隊列默認是Default優先級,蘋果的XNU內核會管理用于Global Dispatch Queue的線程,并把Global Dispatch Queue的優先級設置成線程的優先級。獲取方法如下:
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0)
dispatch_set_target_queue
上一個點有說到通過creat創建的所有隊列默認是Default優先級,那我們想改變它的優先級行不行?行,用dispatch_set_target_queue,用法如下:
dispatch_queue_t serialQueue = dispatch_queue_create("com.mathewchen.queue",NULL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);
dispatch_set_target_queue(serialQueue, globalQueue);
/* * 第一個參數為要設置優先級的queue,第二個參數是參照物,既將第一個queue的優先級和第二個queue的優先級設置一樣。
*/
上面是它的第一個作用,設置queue的優先級。那么第二個作用就有意思了,設置queue的層級。那我閑著沒事給queue設置層級干嘛?設置層級最主要的目的是解決并發問題。
例如現在有一個任務被拆分成ABCDEF五個串行隊列,但實際還是需要這個任務同步執行,那么就會有問題,因為多個串行queue之間是并行的。就像下面這個例子:
dispatch_queue_t queue = dispatch_queue_create("test.mathewchen0.queue", NULL);
······
dispatch_queue_t queue5 = dispatch_queue_create("test.mathewchen5.queue", NULL);
dispatch_async(queue, ^{
//code here
NSLog(@"同步調用開始,當前線程:%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:3.f];
NSLog(@"同步調用結束,當前線程:%@", [NSThread currentThread]);
});
······
dispatch_async(queue5, ^{
//code here
NSLog(@"同步調用開始5,當前線程:%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:3.f];
NSLog(@"同步調用結束5,當前線程:%@", [NSThread currentThread]);
});
打出的LOG如下
2016-09-26 12:26:40.846 testGCD[6593:1851518] 同步調用開始,當前線程:<NSThread: 0x7f97d24285e0>{number = 2, name = (null)}
2016-09-26 12:26:40.846 testGCD[6593:1851528] 同步調用開始3,當前線程:<NSThread: 0x7f97d260b250>{number = 4, name = (null)}
2016-09-26 12:26:40.846 testGCD[6593:1851517] 同步調用開始1,當前線程:<NSThread: 0x7f97d26103c0>{number = 3, name = (null)}
2016-09-26 12:26:40.847 testGCD[6593:1851529] 同步調用開始4,當前線程:<NSThread: 0x7f97d2608cf0>{number = 5, name = (null)}
2016-09-26 12:26:40.847 testGCD[6593:1851510] 同步調用開始2,當前線程:<NSThread: 0x7f97d2567050>{number = 6, name = (null)}
2016-09-26 12:26:40.847 testGCD[6593:1851530] 同步調用開始5,當前線程:<NSThread: 0x7f97d2565090>{number = 7, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851517] 同步調用結束1,當前線程:<NSThread: 0x7f97d26103c0>{number = 3, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851528] 同步調用結束3,當前線程:<NSThread: 0x7f97d260b250>{number = 4, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851529] 同步調用結束4,當前線程:<NSThread: 0x7f97d2608cf0>{number = 5, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851510] 同步調用結束2,當前線程:<NSThread: 0x7f97d2567050>{number = 6, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851518] 同步調用結束,當前線程:<NSThread: 0x7f97d24285e0>{number = 2, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851530] 同步調用結束5,當前線程:<NSThread: 0x7f97d2565090>{number = 7, name = (null)}
很顯然,拆分成的六個queue并沒有等待其他幾個queue,全都并行執行了。解決方法如下,給所有的queue設置一個targer queue:
dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue", NULL);
dispatch_set_target_queue(queue, targetQueue);
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_set_target_queue(queue3, targetQueue);
dispatch_set_target_queue(queue4, targetQueue);
dispatch_set_target_queue(queue5, targetQueue);
這次打出的LOG就符合我們的要求了
2016-09-26 12:33:40.312 testGCD[6643:1855135] 同步調用開始,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:43.316 testGCD[6643:1855135] 同步調用結束,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:43.316 testGCD[6643:1855135] 同步調用開始1,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:46.321 testGCD[6643:1855135] 同步調用結束1,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:46.321 testGCD[6643:1855135] 同步調用開始2,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:49.326 testGCD[6643:1855135] 同步調用結束2,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:49.326 testGCD[6643:1855135] 同步調用開始3,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:52.328 testGCD[6643:1855135] 同步調用結束3,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:52.328 testGCD[6643:1855135] 同步調用開始4,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:55.331 testGCD[6643:1855135] 同步調用結束4,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:55.331 testGCD[6643:1855135] 同步調用開始5,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
2016-09-26 12:33:58.334 testGCD[6643:1855135] 同步調用結束5,當前線程:<NSThread: 0x7fd8c8521250>{number = 2, name = (null)}
dispatch_after
這個方法經常被用做延遲觸發,不過一般用
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
比較多,該方法的使用如下:
dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC);
dispatch_after(time, queue, ^{
NSLog(@"三秒后執行");
});
dispatch_after方法并不是在指定時間后執行處理,只是在指定時間后把添加到queue中,queue每次循環也是有一些小小的間隔,這就會導致一些微小的誤差,不過誤差是毫秒級的,一般使用問題不大。如果想要使用精確時間的話,使用dispatch_walltime即可,具體使用方法就不贅述了。
Dispatch Group
假如有這么一種情況,我需要在ABCD任務執行完以后,再執行任務E。如果是串行隊列的話,我直接在D中調用E就好了。那么問題來了,ABCD是并行隊列的話,以哪一個任務結束作為結束點都不能很好的解決我們的問題。因此,可以將ABCD打包成一個任務,然后塞進串行隊列再進行下一步處理。這是一般的解決思路,蘋果覺得每一次都要這樣處理太過于麻煩,于是提供了一個Dispatch Group來解決類似問題。用法如下:
dispatch_group_t group=dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"block0");
});
dispatch_group_async(group, queue, ^{
NSLog(@"block1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"block2");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"執行完畢");
});
打出LOG
2016-09-27 18:14:41.265 testGCD[7034:1896349] block0
2016-09-27 18:14:41.276 testGCD[7034:1896349] block1
2016-09-27 18:14:41.277 testGCD[7034:1896349] block2
2016-09-27 18:14:41.278 testGCD[7034:1896319] 執行完畢
創建什么的跟隊列差不多就不贅述了,我們將queue打包進group,當group中的任務執行完畢以后會執行notify中的代碼。
最后再提一下group中比較常用的dispatch_group_wait方法,這個方法比較有意思,它是用來判斷Group是否執行完畢。它有兩個參數,第一個參數就是監聽的group,第二個參數是超時時間。當時間超過超時時間時,該方法會返回一個非0值,代表Group中還有任務沒有處理完畢,反之0就是處理完畢,可以用這個方法做一些耗時任務的判斷,萬一太耗時了就把它cancel掉。
dispatch_barrier_async
多線程并發問題除了死鎖,還有一個數據競爭問題。最常見的就是數據庫寫入操作時,不可以與其他寫入或讀取操作一起執行。要是懶一點的程序員,直接在數據庫的寫入或者讀取的時候上個鎖就好了,讓某一時間下對數據庫的操作只能有一個。但是,同時讀取數據庫是可以的,而且為了提高讀取的效率,我們最好支持同時讀取。我們前面講的group、settarget都可以解決這個問題,只是比較麻煩而已,現在來介紹另一種方法解決,那就是barrier。
舉例說明一下用法。看下面代碼
dispatch_async(queue,doread);
dispatch_async(queue,doread1);
dispatch_async(queue,doread2);
dispatch_async(queue,doread3);
dispatch_async(queue,doread4);
dispatch_async(queue,doread5);
我們現在有6個并行處理的讀取操作,假如現在需要進行寫入操作,我們只需要在某一個地方插入dispatch_barrier_async,如下面所示:
····
dispatch_async(queue,doread2);
dispatch_barrier_async(queue,dowrite);
dispatch_async(queue,doread3);
····
當我們的代碼執行到dispatch_barrier_async時,它會先等待已經在queue中處理的任務處理完畢,再把自己的任務放入queue,同時將剩下的任務給阻攔起來(barrier),相當于一個串行隊列阻塞的作用。當自己的任務執行完畢,我們的queue才又恢復正常的工作,這也是一種處理多線程并發問題的解決方案。
NSOperation
NSOperation是蘋果公司對于GCD的封裝,雖然理念和GCD一樣,都是通過隊列+操作的形式,但是對程序員友好多了。
在NSOperation中,任務需要被封裝成一個對象才能被執行。可以封裝成NSInvocationOperation或者NSBlockOperation,這倆貨都是NSOperation的子類。你可以將方法名傳入,就像這樣
NSInvocationOperation *operation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(onTest) object:nil];
[operation start];
你也可以這樣,把一個block傳入
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"6666");
}];
[operation start];
但是推薦使用NSBlockOperation,因為NSInvocationOperation不是類型安全,在swift中已經去掉這個類了,OC感覺也快了。很明顯可以看出,start是開始執行任務的標志。當我們調用start方法的時候,會默認在當前線程同步執行,這就相當于我們上文GCD的同步操作串行/并行隊列。
那我需要異步操作并行隊列怎么辦呢?在NSBlockOperation中還有一個addExecutionBlock方法,調用這個方法就可以達到我們的目標。
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
[operation start];
打出的LOG和我們異步調用并行隊列一模一樣
2016-09-28 15:17:06.764 testGCD[7812:2042404] <NSThread: 0x7f8aea402780>{number = 1, name = main}
2016-09-28 15:17:06.766 testGCD[7812:2042450] 第1次:<NSThread: 0x7f8aea561ba0>{number = 3, name = (null)}
2016-09-28 15:17:06.766 testGCD[7812:2042458] 第2次:<NSThread: 0x7f8aea557540>{number = 4, name = (null)}
2016-09-28 15:17:06.768 testGCD[7812:2042404] 第3次:<NSThread: 0x7f8aea402780>{number = 1, name = main}
2016-09-28 15:17:06.765 testGCD[7812:2042437] 第0次:<NSThread: 0x7f8aea561210>{number = 2, name = (null)}
2016-09-28 15:17:06.770 testGCD[7812:2042450] 第4次:<NSThread: 0x7f8aea561ba0>{number = 3, name = (null)}
但是不推薦用這個方法,有替代的方法下面說。
NSOperationQueue
上面的例子其實還差一個異步調用串行隊列,但是在講這個用法之前,先把NSOperationQueue講了,因為我們上面都是把任務封裝了一下,看起來沒隊列什么事就執行了,事實上那些任務都是被包裹在隊列中被執行的,因此在講異步調用串行隊列的使用之前,先把隊列這里給講了。
同GCD一樣,NSOperationQueue也分為兩種隊列,串行和并行,而串行隊列中最典型的就是main隊列,如下所示:
NSOperationQueue *queue = [NSOperationQueue mainQueue];
剩下的其他隊列:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
···
[queue addOperation:operation];
這樣就把任務添加到了隊列之中,另外再補充一點,所有添加到隊列的任務都會自動調用start方法。
下面就是重點了(敲黑板),NSOperation對GCD做了封裝,把難理解的同步異步線程給整合成一個參數maxConcurrentOperationCount。這個參數能干什么呢,它能控制Operation的最大并發數,通過控制最大并發數,我們就能控制線程數。因為同時只能有一個Operation執行的話,系統只會給我們開一個線程,這樣就相當于GCD的異步串行操作。就像這樣
queue.maxConcurrentOperationCount=1;
OK,我覺得要是較真的人現在已經開始把這段代碼加入之前的并行異步操作的代碼進行嘗試了,但是發現不起作用,也就是最大并發數失效。這就是上面不推薦用那段代碼并行操作的原因。maxConcurrentOperationCount,字面翻譯都是最大的ConcurrentOperation數量,而我們看看上面的代碼,明明只有一個Operation,只不過我們在里面給Operation封了很多任務而已。maxConcurrentOperationCount只能控制Operation的數量,并不能控制Operation中任務的并發數量,所以導致了maxConcurrentOperationCount失效。OK,那正常的使用方法是怎樣,如下所示:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// queue.maxConcurrentOperationCount=1;
for (NSInteger i = 0; i < 5; i++) {
[queue addOperationWithBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
我們直接通過NSOperationQueue中的addOperationWithBlock把block封入一個Operation,最后把這一堆Operation放入queue中,而不是像之前那樣把一堆block封入一個Operation,再把Operation放入queue,這樣最大并發數就無法控制了。
添加依賴
GCD中添加block的執行先后比較繁瑣,而在NSOperation中則簡單多了。它把執行的先后做成一個API接口,通過API接口我們可以輕松的設置block執行順序。這個接口就是addDependency,添加依賴。用法如下:
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"A - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"B - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"C - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//B任務添加依賴A
[operation2 addDependency:operation1];
//C任務添加依賴B
[operation3 addDependency:operation2];
//所以執行順序就是ABC
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
特別注意,千萬別相互依賴,會死鎖!!!!
其他用法
- NSOperation
BOOL executing; //判斷任務是否正在執行
BOOL finished; //判斷任務是否完成
void (^completionBlock)(void); //用來設置完成后需要執行的操作
- (void)cancel; //取消任務
- (void)waitUntilFinished; //阻塞當前線程直到此任務執行完畢
- NSOperationQueue
NSUInteger operationCount; //獲取隊列的任務數
- (void)cancelAllOperations; //取消隊列中所有的任務
- (void)waitUntilAllOperationsAreFinished; //阻塞當前線程直到此隊列中的所有任務執行完畢
[queue setSuspended:YES]; // 暫停queue
[queue setSuspended:NO]; // 繼續queue
優劣比較
多線程編程技術的優缺點比較
-
NSThread (抽象層次:低)
- 優點:輕量級,簡單易用,可以直接操作線程對象
- 缺點: 需要自己管理線程的生命周期,線程同步。線程同步對數據的加鎖會有一定的系統開銷。
-
Cocoa NSOperation (抽象層次:中)
- 優點:不需要關心線程管理,數據同步的事情,可以把精力放在學要執行的操作上。基于GCD,是對GCD 的封裝,比GCD更加面向對象
- 缺點: NSOperation是個抽象類,使用它必須使用它的子類,可以實現它或者使用它定義好的兩個子類NSInvocationOperation、NSBlockOperation.
-
GCD 全稱Grand Center Dispatch (抽象層次:高)
- 優點:是 Apple 開發的一個多核編程的解決方法,簡單易用,效率高,速度快,基于C語言,更底層更高效,并且不是Cocoa框架的一部分,自動管理線程生命周期(創建線程、調度任務、銷毀線程)。
- 缺點: 使用GCD的場景如果很復雜,就有非常大的可能遇到死鎖問題。
常見并發問題及其解決方案
死鎖
死鎖在多線程編程中一不留神就出現了,例如下面這段代碼就是典型的死鎖:
dispatch_sync(dispatch_get_main_queue(), ^{
//造成死鎖了
});
主線程等待主線程執行完畢再執行block,自己等待自己結果造成了死循環,如果遇到LOG打不出來的情況,第一個就要檢查queue是否循環等待了。
互斥
這個也是常見的,一件事不能倆線程同時干,同時就會出問題。一般這樣情況就可以使用@synchronized給代碼加上互斥鎖。
@synchronized(self){
}
同步執行
拿GCD來說,如果是在一個類里還比較好同步,但是項目大了以后,有可能很多地方都需要同步,這時候通常的做法就是設置一個全局queue來處理同步問題:
dispatch_sync(queue, ^{
NSInteger times = lastTimes;
[NSThread sleepForTimeInterval:1];
NSLog(@"調用次數:%ld ,調用線程: %@",times, [NSThread currentThread]);
times -= 1;
lastTimes = times;
});