知其然亦知其因此然--NSOperation併發編程 (轉)

本文是投稿文章,做者:RyanJIN(簡書
對於iOS的併發編程, 用的最廣泛的就是GCD了, GCD結合Block能夠so easy的實現多線程併發編程. 但若是你看一些諸如AFNetworking, SDWebImage的源碼, 你會發現它們使用的都是NSOperation, 納尼? 難道NSOperation這貨更屌? YES, 它確實更屌! Okay, 那咱們就先來簡單PK下GCD和NSOperation(固然這裏也包括NSOperationQueue).html

1). NSOperation是基於GCD之上的更高一層封裝, 擁有更多的API(e.g. suspend, resume, cancel等等).git

2). 在NSOperationQueue中, 能夠指定各個NSOperation之間的依賴關係.github

3). 用KVO能夠方便的監測NSOperation的狀態(isExecuted, isFinished, isCancelled).編程

4). 更高的可定製能力, 你能夠繼承NSOperation實現可複用的邏輯模塊.swift

Soga, 原來NSOperation這麼拽! Apple官方文檔和網絡上有不少NSOperation的資料, 但大部分都是很書面化的解釋(臣妾看不懂啊%>_<%), 看着看着就雲深不知處了. 因此這篇文章我會以灰常通俗的方式來解釋NSOperation的併發編程. Okay, let's go!api

併發編程的幾個概念網絡

併發編程簡單來講就是讓CPU在同一時間運行多個任務. 這裏面有幾個容易混淆的概念, 咱們先來一個個的梳理下:多線程

1). 串行(Serial) VS. 並行(Concurrent)併發

串行和並行描述的是任務和任務之間的執行方式. 串行是任務A執行完了任務B才能執行, 它們倆只能順序執行. 並行則是任務A和任務B能夠同時執行.異步

2). 同步(Synchronous) VS. 異步(Asynchronous)

同步和異步描述的其實就是函數何時返回. 好比用來下載圖片的函數A: {download image}, 同步函數只有在image下載結束以後才返回, 下載的這段時間函數A只能搬個小板凳在那兒坐等... 而異步函數, 當即返回. 圖片會去下載, 但函數A不會去等它完成. So, 異步函數不會堵塞當前線程去執行下一個函數!

3). 併發(Concurrency) VS. 並行(Parallelism)

這個更容易混淆了, 先用Ray大神的示意圖和說明來解釋一下: 併發是程序的屬性(property of the program), 而並行是計算機的屬性(property of the machine).

blob.png

仍是很抽象? 那我再來解釋一下, 並行和併發都是用來讓不一樣的任務能夠"同時執行", 只是並行是僞同時, 而併發是真同時. 假設你有任務T1和任務T2(這裏的任務能夠是進程也能夠是線程):

a. 首先若是你的CPU是單核的, 爲了實現"同時"執行T1和T2, 那隻能分時執行, CPU執行一下子T1後立刻再去執行T2, 切換的速度很是快(這裏的切換也是須要消耗資源的, context switch), 以致於你覺得T1和T2是同時執行了(但其實同一時刻只有一個任務佔有着CPU).

b. 若是你是多核CPU, 那麼恭喜你, 你能夠真正同時執行T1和T2了, 在同一時刻CPU的核心core1執行着T1, 而後core2執行着T2, great!

其實咱們日常說的併發編程包括狹義上的"並行"和"併發", 你不能保證你的代碼會被並行執行, 但你能夠以併發的方式設計你的代碼. 系統會判斷在某一個時刻是否有可用的core(多核CPU核心), 若是有就並行(parallelism)執行, 不然就用context switch來分時併發(concurrency)執行. 最後再以Ray大神的話結尾: Parallelism requires Concurrency, but Concurrency does not guarantee Parallelism!

併發吧, NSOperation!

NSOperation能夠本身獨立執行(直接調用[operation start]), 也能夠放到NSOperationQueue裏面執行, 這兩種狀況下是否併發執行是不一樣的. 咱們先來看看NSOperation獨立執行的併發狀況.

1. 獨立執行的NSOperation

NSOperation默認是非併發的(non-concurrent), 也就說若是你把operation放到某個線程執行, 它會一直block住該線程, 直到operation finished. 對於非併發的operation你只須要繼承NSOperation, 而後重寫main()方法就妥妥滴了, 好比咱們用非併發的operation來實現一個下載需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation YourOperation 
- (void)main 
{
     @autoreleasepool {
         if  (self.isCancelled)  return ;
         NSData *imageData = [[NSData alloc] initWithContentsOfURL:imageURL];
         if  (self.isCancelled) { imageData = nil;  return ; }
         if  (imageData) {
             UIImage *downloadedImage = [UIImage imageWithData:imageData];
         }
         imageData = nil;
         if  (self.isCancelled)  return ;
         [self.delegate performSelectorOnMainThread:@selector(imageDownloaderDidFinish:)                                                                  
                                         withObject:downloadedImage
                                      waitUntilDone:NO];
     }
}
@end

因爲NSOperation是能夠cancel的, 因此你須要在operation程序內部執行過程當中判斷當前operation是否已經被cancel了(isCancelled). 若是已經被cancel那就不往下執行了. 當你在外面調用[operation cancel]後, isCancelled會被置爲YES.

NSOperation有三個狀態量isCancelled, isExecuting和isFinished. isCancelled上面解釋過. main函數執行完成後, isExecuting會被置爲NO, 而isFinished則被置爲YES.

那腫麼實現併發(concurrent)的NSOperation呢? 也很簡單:

1). 重寫isConcurrent函數, 返回YES, 這個告訴系統各單位注意了我這個operation是要併發的.

2). 重寫start()函數.

3). 重寫isExecuting和isFinished函數

爲何在併發狀況下須要本身來設定isExecuting和isFinished這兩個狀態量呢? 由於在併發狀況下系統不知道operation何時finished, operation裏面的task通常來講是異步執行的, 也就是start函數返回了operation不必定就是finish了, 這個你本身來控制, 你何時將isFinished置爲YES(發送相應的KVO消息), operation就何時完成了. Got it? Good.

仍是上面那個下載的例子, 咱們用併發的方式來實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (BOOL)isConcurrent {
     return  YES;
}
- (void)start 
{
     [self willChangeValueForKey:@ "isExecuting" ];
     _isExecuting = YES;
     [self didChangeValueForKey:@ "isExecuting" ];
     NSURLRequest * request = [NSURLRequest requestWithURL:imageURL];
     _connection = [[NSURLConnection alloc] initWithRequest:request
                                                   delegate:self];
     if  (_connection == nil) [self finish];
}
- (void)finish
{
     self.connection = nil;
     [self willChangeValueForKey:@ "isExecuting" ];
     [self willChangeValueForKey:@ "isFinished" ];
     _isExecuting = NO;
     _isFinished = YES;
     [self didChangeValueForKey:@ "isExecuting" ];
     [self didChangeValueForKey:@ "isFinished" ];
}
#pragma mark - NSURLConnection delegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
     // to do something...
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
     // to do something...
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
     [self finish];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
     [self finish];
}
@end

Wow, 並行的operation好像有那麼點意思了. 這裏面還有幾點須要mark一下:

a). operation的executing和finished狀態量須要用willChangeValueForKey/didChangeValueForKey來觸發KVO消息.

b). 在調用完NSURLConnection以後start函數就返回了, 後面就坐等connection的回調了.

c). 在connection的didFinish或didFail回調裏面設置operation的finish狀態, 告訴系統operation執行完畢了.

若是你是在主線程調用的這個併發的operation, 那一切都是很是的perfect, 就算你當前在操做UI也不影響operation的下載操做. BUT, 若是你是在子線程調用的, 或者把operation加到了非main queue, 那麼問題來了, 你會發現這貨的NSURLConnection delegate不走了, what's going on here? 要解釋這個問題就要請出另一個武林高手NSRunLoop, Okay, 下面進入NSRunLoop的show time.

Hey, NSRunLoop你是神馬東東?

關於NSRunLoop推薦看一下孫源@sunnnyxx的分享視頻. 其實從字面上就能夠看出來, RunLoop就是跑圈, 保證程序一直在執行. App運行起來以後, 即便你什麼都不作, 放在那兒它也不會退出, 而是一直在"跑圈", 這就是RunLoop乾的事. 主線程會自動建立一個RunLoop來保證程序一直運行. 但子線程默認不建立NSRunLoop, 因此子線程的任務一旦返回, 線程就over了.

上面的併發operation當start函數返回後子線程就退出了, 當NSURLConnection的delegate回調時, 線程已經木有了, 因此你也就收不到回調了. 爲了保證子線程持續live(等待connection回調), 你須要在子線程中加入RunLoop, 來保證它不會被kill掉.

RunLoop在某一時刻只能在一種模式下運行, 更換模式時須要暫停當前的Loop, 而後重啓新的Loop. RunLoop主要有下面幾個模式:

  • NSDefalutRunLoopMode : 默認Mode, 一般主線程在這個模式下運行

  • UITrackingRunLoopMode : 滑動ScrollView是會切換到這個模式

  • NSRunLoopCommonModes: 包括上面兩個模式

這邊須要特別注意的是, 在滑動ScrollView的狀況下, 系統會自動把RunLoop模式切換成UITrackingRunLoopMode來保證ScrollView的流暢性.

1
2
3
4
5
[NSTimer scheduledTimerWithTimeInterval:1.f
                                  target:self
                                selector:@selector(timerAction:)   
                                userInfo:nil
                                 reports:YES];

當你在滑動ScrollView的時候上面的timer會失效, 緣由是Timer是默認加在NSDefalutRunLoopMode上的, 而滑動ScrollView後系統把RunLoop切換爲UITrackingRunLoopMode, 因此timer就不會執行了. 解決方法是把該Timer加到NSRunLoopCommonModes下, 這樣即便滑動ScrollView也不會影響timer了.

1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

另外還有一個trick是當tableview的cell從網絡異步加載圖片, 加載完成後在主線程刷新顯示圖片, 這時滑動tableview會形成卡頓. 一般的思路是tableview滑動的時候延遲加載圖片, 等中止滑動時再顯示圖片. 這裏咱們能夠經過RunLoop來實現.

1
2
3
4
[self.cellImageView performSelector:@sector(setImage:)
                          withObject:downloadedImage
                          afterDelay:0
                             inModes:@[NSDefaultRunLoopMode]];

當NSRunLoop爲NSDefaultRunLoopMode的時候tableview確定中止滑動了, why? 由於若是還在滑動中, RunLoop的mode應該是UITrackingRunLoopMode.

好了, 既然咱們已經瞭解RunLoop的東東了, 咱們能夠回過頭來解決上面子線程併發NSOperation下NSURLConnection的Delegate不走的問題, 各位童鞋且繼續往下看^_^

呼叫NSURLConnection的異步回調

如今解決方案已經很清晰了, 就是利用RunLoop來監督線程, 讓它一直等待delegate的回調. 上面已經說到Main Thread是默認建立了一個RunLoop的, 因此咱們的Option 1是讓start函數在主線程運行(即便[operation start]是在子線程調用的).

1
2
3
4
5
6
7
8
9
10
- (void)start 
{
     if  (![NSThread isMainThread]) {
         [self performSelectorOnMainThread:@selector(start)
                                withObject:nil
                             waitUntilDone:NO];
         return ;
     }
     // set up NSURLConnection...
}

或者這樣:

1
2
3
4
5
6
- (void)start
{
     [[NSOperationQueue mainQueue] addOperationWithBlock:^{
         self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
     }];
}

這樣咱們能夠簡單直接的使用main run loop, 由於數據delivery是很是快滴. 而後咱們就能夠將處理incoming data的操做放到子線程去...

Option 2是讓operation的start函數在子線程運行, 可是咱們爲它建立一個RunLoop. 而後把URL connection schedule到上面去. 咱們先來瞅瞅AFNetworking是怎麼作滴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
+ (void)networkRequestThreadEntryPoint:(id)__unused object 
{
     @autoreleasepool {
         [[NSThread currentThread] setName:@ "AFNetworking" ];
         NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
         [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
         [runLoop run];
     }
}
+ (NSThread *)networkRequestThread 
{
     static NSThread *_networkRequestThread = nil;
     static dispatch_once_t oncePredicate;
     dispatch_once(&oncePredicate, ^{
         _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
         [_networkRequestThread start];
     });
     return  _networkRequestThread;
}
- (void)start 
{
     [self.lock lock];
     if  ([self isCancelled]) {
         [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
     else  if  ([self isReady]) {
         self.state = AFOperationExecutingState;
         [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
     }
     [self.lock unlock];
}

AFNetworking建立了一個新的子線程(在子線程中調用NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 獲取RunLoop對象的時候, 就會建立RunLoop), 而後把它加到RunLoop裏面來保證它一直運行.

這邊咱們能夠簡單的判斷下當前start()的線程是子線程仍是主線程, 若是是子線程則調用[NSRunLoop currentRunLoop]創新RunLoop, 不然就直接調用[NSRunLoop mainRunLoop], 固然在主線程下就不必調用[runLoop run]了, 由於它原本就是一直run的.

P.S. 咱們還可使用CFRunLoop來啓動和中止RunLoop, 像下面這樣:

1
2
3
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop]
                            forMode:NSRunLoopCommonModes];
CFRunLoopRun();

等到該Operation結束的時候, 必定要記得調用CFRunLoopStop()中止當前線程的RunLoop, 讓當前線程在operation finished以後能夠退出.

2. NSOperationQueue裏面執行NSOperation

NSOpertion能夠add到NSOperationQueue裏面讓Queue來觸發其執行, 一旦NSOperation被add到Queue裏面那麼咱們就不care它自身是否是併發設計的了, 由於被add到Queue裏面的operation一定是併發的. 並且咱們能夠設置Queue的maxConcurrentOperationCount來指定最大的併發數(也就是幾個operation能夠同時被執行, 若是這個值設爲1, 那這個Queue就是串行隊列了).

爲嘛添加到Queue裏面的operation必定會是併發執行的呢? Queue會爲每個add到隊列裏面的operation建立一個線程來運行其start函數, 這樣每一個start都分佈在不一樣的線程裏面來實現operation們的併發執行.

重要的事情再強調一遍: 咱們這邊所說的併發都是指NSOperation之間的併發(多個operation同時執行), 若是maxConcurrentOperationCount設置爲1或者把operation放到[NSOperationQueue mainQueue]裏面執行, 那它們只會順序(Serial)執行, 固然就不可能併發了.

[NSOperationQueue mainQueue]返回的主隊列, 這個隊列裏面任務都是在主線程執行的(固然若是你像AFNetworking同樣在start函數建立子線程了, 那就不是在主線程執行了), 並且它會忽略一切設置讓你的任務順序的非併發的執行, 因此若是你把NSOperation放到mainQueue裏面了, 那你就放棄吧, 無論你怎麼折騰, 它是絕對不會併發滴. 固然, 若是是[[NSOperationQueue alloc] init]那就是子隊列(子線程)了.

那...那不對呀, 若是我在子線程調用[operation start]函數, 或者把operation放到非MainQueue裏面執行, 可是在operation的內部把start拋到主線程來執行(利用主線程的main run loop), 那多個operation其實不都是要在主線程執行的麼, 這樣還能併發? Luckily, 仍然是併發執行的(其實我想說的是那必須能併發啊...哈哈).

咱們能夠先來看看單線程和多線程下的各個任務(task)的併發執行示意圖:

blob.png

Yes! 和上面討論狹義併發(Concurency)和並行(Parallelism)概念時的理解是同樣的, 在單線程狀況下(也就是mainQueue的主線程), 各個任務(在咱們這裏就是一個個的NSOperation)能夠經過分時來實現僞並行(Parallelism)執行.

blob.png

而在多線程狀況下, 多個線程同時執行不一樣的任務(各個任務也會不停的切換線程)實現task的併發執行.

另外, 咱們在往Queue裏面添加operation的時候能夠指定它們的依賴關係, 好比[operationB addDependency:operationA], 那麼operationB會在operationA執行完畢以後纔會執行. 還記得這邊"執行完畢(isFinished)"的概念嗎? 在併發狀況下這個狀態量是由你本身設定的, 好比operationA是用來異步下載一張圖片, 那麼只有圖片下載完成以後或者超過timeout下載失敗以後, isFinished狀態量被標記爲YES, 這時Queue纔會從隊列裏面移除operationA, 並啓動operationB. 是否是很cool? O(∩_∩)O~~

NSOperation實驗課

下面咱們進入實驗課啦, 要想真正瞭解某個東東, 仍是須要打開Xcode, 寫上幾行代碼, 而後Commard+R. 爲了幫Apple提高Xcode的使用率:-D, 我會給出幾個case, 童鞋們能夠本身編寫test code來驗證:

1). 建立兩個operation, 而後直接[operation start], 在NSOperation併發設計和非併發設計的狀況下, 查看這兩個operation是否同時執行了(最簡單的打log看是否是交替打印).

2). 在主線程和子線程下分別調用[operation start], 看看執行狀況.

3). 建立operation並放到NSOperationQueue裏面執行, 分別看看mainQueue和非mainQueue下的執行狀況.

4). maxConcurrentOperationCount設置後的執行狀況.

5). 試試NSOperation的依賴關係設置, [operationB addDependency:operationA].

6). 寫個完整的demo吧, 好比簡單的HTTP Downloader.

最後送上乾貨Demo, RJHTTPDownloader, 用NSOperation實現的一個下載類. 有的童鞋確定會說用AFNetwroking就能夠了, 爲嘛要本身去寫呢? 這個嘛, 偶是以爲別人的代碼再怎麼看和用都不是你的, 本身動手寫的才真正belongs to you! 並且這也不算是重複造輪子, 只是學習輪子是怎麼構造的, 這樣一步一步的慢慢積累, 總有一天咱們也能寫出像AFNetworking這樣的代碼! 共勉.

相關文章
相關標籤/搜索