被遺棄的執行緒

騎著jm的hi發表於2019-03-01

原文地址

main函式作為程式執行的入口,正常情況下,函式會執行毫秒級別的操作,然後返回一個0表示程式正常終止。為了避免應用啟動即終止,蘋果設計了runloop機制來維持執行緒的生命,runloop在每一次的迴圈當中不斷的去處理事件,或控制執行緒的休眠和喚醒。runloop還結合了libdispatch的任務派發機制,可以迴圈地處理async到佇列中的任務

啟動runloop

runloop對外暴露的介面來看,啟動方式一共存在三種:

  • 無條件的啟動。這種啟動方式缺少停止runloop的手段,唯一的結束方式是kill掉執行緒

      - (void)run;
    複製程式碼
  • 設定超時時間。直接run會呼叫這個介面並且傳入distantFuture,表示無限長的超時時間。如果超時時間不是無限長,那麼runloop會在處理完事件或者超時後終止,優於直接run

      - (void)runUntilDate: (NSDate *)limitDate;
    複製程式碼
  • 設定超時時間和執行模式。比起第二種,允許我們讓runloop執行在某個模式下,靈活性更高

      - (BOOL)runMode: (NSRunLoopMode)mode beforeDate: (NSDate *)limitDate;
    複製程式碼

如果runloop在啟動之後沒有任何sourcestimers或者ports事件可以處理,那麼會自動退出,否則會在處理完成後讓執行緒陷入休眠,等待這些事件重新喚醒執行緒處理。下面是最常用來表示runloop處理邏輯的示意圖:

被遺棄的執行緒

除開圖中列出的事件之外,main loop會處理timer之後檢測佇列中是否存在待執行的block然後開始執行

什麼情況下需要啟動runloop

主執行緒的runloop會在應用啟動後被UIApplication啟動,其他執行緒則需要我們主動去run。從接觸iOS開發到現在,筆者瞭解的需要主動啟動runloop只有這麼兩類:

  • 子執行緒使用timer

    由於NSTimer本身就不是一個能確保穩定回撥的定時器機制,並且主執行緒會經常處在忙碌狀態,這又進一步降低了NSTimer的準確性。為了提高定時的準確性,多數人會採用子執行緒啟動runloop的方式來實現定時器功能

  • 執行緒保活

    執行緒保活是一種不太常見的需求,但是如果你曾經瞭解過AFNetworking的做法,會發現建立了子執行緒之後,採用一個空port的方式來啟動runloop,避免執行緒被中止回收。但實際上這種做法很容易導致執行緒既無法被回收,也不能被使用的情況

上面兩種不同的應用場景,實際上是使用timerport維持runloop不會因為沒有事件處理直接退出,而且在這些源事件來臨之前,執行緒大多數情況下處在休眠狀態不造成額外損耗

序列佇列的runloop

假設現在需要使用一個子執行緒的runloop來實現定時器,由於runloop在停止之前,執行緒會一直存活,因此可能會想利用這個存活的執行緒處理其他的任務。因此除了NSTimer之外,我們新增一個GCD Timer定時的派發任務給這個啟動runloop的佇列:

dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
    NSLog(@"the task run in the thread: %d", mach_thread_self());
    [NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: YES block: ^(NSTimer * _Nonnull timer) {
        NSLog(@"ns timer in the thread: %d", mach_thread_self());
    }];
    [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 600]];
});

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, serialQueue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"gcd timer in the thread: %d", mach_thread_self());
});
dispatch_resume(timer);
複製程式碼

按照預期,這段程式碼充分利用了已經被保活的執行緒,除了已有的NSTimer之外,執行緒還能在空閒的時間去處理不斷派發的任務,但實際上只有NSTimer的任務被執行:

2018-04-14 10:32:54.667718+0800 PThreads[7693:94702] the task run in the thread: 4355
2018-04-14 10:32:55.168871+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:55.672011+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:56.169150+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:56.669411+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:57.169665+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:57.669234+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:58.172068+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:58.669446+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:59.169223+0800 PThreads[7693:94702] ns timer in the thread: 4355
2018-04-14 10:32:59.671802+0800 PThreads[7693:94702] ns timer in the thread: 4355
複製程式碼

導致保活執行緒無法處理async任務的原因有兩個:

  • runloopqueue的區別

    runloopqueue各自維護著自己的一個任務佇列,在runloop的每個週期裡面,會檢測自身的任務佇列裡面是否存在待執行的task並且執行。但主執行緒的情況比較特殊,在main runloop的每個週期,會去檢測main queue是否存在待執行任務,如果存在,那麼copy到自身的任務佇列中執行

  • async的實現不同

    在非主執行緒之外,runloopqueue的任務佇列是互不干擾的,因此兩者處理任務的機制也是完全不同的。當async任務到佇列時,GCD會嘗試尋找一個執行緒來執行任務。由於序列佇列同時只能與一個執行緒掛鉤,因此GCD會讓該執行緒執行完已有任務後,才執行async到佇列中的任務。但由於執行緒被保活,任務是一個條件死迴圈condition-loop,因此async的任務始終無法被處理

為了證明這些原因,可以通過CFRunLoopPerformBlock將任務直接加入到runloop自身的任務佇列中,檢測這個任務是否被執行:

__block CFRunLoopRef serialRunLoop = NULL;
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{
    NSLog(@"the task run in the thread: %d", mach_thread_self());
    [NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: YES block: ^(NSTimer * _Nonnull timer) {
        NSLog(@"ns timer in the thread: %d", mach_thread_self());
    }];
    serialRunLoop = [NSRunLoop currentRunLoop].getCFRunLoop;
    [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 600]];
});

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    dispatch_async(serialQueue, ^{
        NSLog(@"gcd timer in the thread: %d", mach_thread_self());
    });
    CFRunLoopPerformBlock(serialRunLoop, NSDefaultRunLoopMode, ^{
        NSLog(@"perform block in thread: %d", mach_thread_self());
    });
});
dispatch_resume(timer);
複製程式碼

再次執行之後,async的任務依舊無法被處理,但是perform block的任務總是能在timer喚醒休眠的執行緒後被處理:

2018-04-14 11:01:29.925924+0800 PThreads[15619:198121] the task run in the thread: 4355
2018-04-14 11:01:30.428175+0800 PThreads[15619:198121] ns timer in the thread: 4355
2018-04-14 11:01:30.428982+0800 PThreads[15619:198121] perform block in thread: 4355
2018-04-14 11:01:30.429410+0800 PThreads[15619:198121] perform block in thread: 4355
2018-04-14 11:01:30.932411+0800 PThreads[15619:198121] ns timer in the thread: 4355
2018-04-14 11:01:30.932674+0800 PThreads[15619:198121] perform block in thread: 4355
2018-04-14 11:01:31.430281+0800 PThreads[15619:198121] ns timer in the thread: 4355
2018-04-14 11:01:31.430546+0800 PThreads[15619:198121] perform block in thread: 4355
2018-04-14 11:01:31.929485+0800 PThreads[15619:198121] ns timer in the thread: 4355
2018-04-14 11:01:31.929691+0800 PThreads[15619:198121] perform block in thread: 4355
2018-04-14 11:01:32.432369+0800 PThreads[15619:198121] ns timer in the thread: 4355
2018-04-14 11:01:32.432726+0800 PThreads[15619:198121] perform block in thread: 4355
2018-04-14 11:01:32.930981+0800 PThreads[15619:198121] ns timer in the thread: 4355
2018-04-14 11:01:32.931179+0800 PThreads[15619:198121] perform block in thread: 4355
2018-04-14 11:01:33.429207+0800 PThreads[15619:198121] ns timer in the thread: 4355
2018-04-14 11:01:33.429519+0800 PThreads[15619:198121] perform block in thread: 4355
複製程式碼

並行佇列的runloop

佇列的串並行屬性決定了佇列能不能被多個執行緒處理任務,因此同樣的程式碼在並行佇列執行,產生的結果必然是有所區別的:

__block CFRunLoopRef serialRunLoop = NULL;
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t serialQueue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(serialQueue, ^{
    NSLog(@"the task run in the thread: %d", mach_thread_self());
    [NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: YES block: ^(NSTimer * _Nonnull timer) {
        NSLog(@"ns timer in the thread: %d", mach_thread_self());
    }];
    serialRunLoop = [NSRunLoop currentRunLoop].getCFRunLoop;
    [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 600]];
});

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    dispatch_async(serialQueue, ^{
        NSLog(@"gcd timer in the thread: %d", mach_thread_self());
    });
    CFRunLoopPerformBlock(serialRunLoop, NSDefaultRunLoopMode, ^{
        NSLog(@"perform block in thread: %d", mach_thread_self());
    });
});
dispatch_resume(timer);
複製程式碼

輸出結果如下:

2018-04-14 11:02:47.348083+0800 PThreads[15988:203200] the task run in the thread: 4867
2018-04-14 11:02:47.397174+0800 PThreads[15988:203203] gcd timer in the thread: 3843
2018-04-14 11:02:47.840678+0800 PThreads[15988:203206] gcd timer in the thread: 9219
2018-04-14 11:02:47.852870+0800 PThreads[15988:203200] ns timer in the thread: 4867
2018-04-14 11:02:47.853122+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:47.853552+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:48.340602+0800 PThreads[15988:203206] gcd timer in the thread: 9219
2018-04-14 11:02:48.352863+0800 PThreads[15988:203200] ns timer in the thread: 4867
2018-04-14 11:02:48.353149+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:48.840085+0800 PThreads[15988:203206] gcd timer in the thread: 9219
2018-04-14 11:02:48.853918+0800 PThreads[15988:203200] ns timer in the thread: 4867
2018-04-14 11:02:48.854120+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:49.340729+0800 PThreads[15988:203206] gcd timer in the thread: 9219
2018-04-14 11:02:49.354172+0800 PThreads[15988:203200] ns timer in the thread: 4867
2018-04-14 11:02:49.354470+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:49.840661+0800 PThreads[15988:203206] gcd timer in the thread: 9219
2018-04-14 11:02:49.853115+0800 PThreads[15988:203200] ns timer in the thread: 4867
2018-04-14 11:02:49.853288+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:50.340078+0800 PThreads[15988:203206] gcd timer in the thread: 9219
2018-04-14 11:02:50.354262+0800 PThreads[15988:203200] ns timer in the thread: 4867
2018-04-14 11:02:50.354537+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:50.840653+0800 PThreads[15988:203206] gcd timer in the thread: 9219
2018-04-14 11:02:50.854117+0800 PThreads[15988:203200] ns timer in the thread: 4867
2018-04-14 11:02:50.854406+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:51.339972+0800 PThreads[15988:203206] gcd timer in the thread: 9219
2018-04-14 11:02:51.353246+0800 PThreads[15988:203200] ns timer in the thread: 4867
2018-04-14 11:02:51.353472+0800 PThreads[15988:203200] perform block in thread: 4867
2018-04-14 11:02:51.839917+0800 PThreads[15988:203206] gcd timer in the thread: 9219
複製程式碼

雖然並行佇列的async功能並不會因為啟動了runloop受到影響,但是可以發現如果不去儲存runloop,這個保活的執行緒除了定時器能正常處理之外,其他時候不會再被GCD複用

使用port保活

如果不使用NSTimer這種穩定的喚醒機制來保活執行緒,而是採用port的方式,執行緒的表現是否依舊符合預期?

__block CFRunLoopRef serialRunLoop = NULL;
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t serialQueue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(serialQueue, ^{
    serialRunLoop = [NSRunLoop currentRunLoop].getCFRunLoop;
    [[NSRunLoop currentRunLoop] addPort: [NSPort new] forMode: NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 600]];
});

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    CFRunLoopPerformBlock(serialRunLoop, NSDefaultRunLoopMode, ^{
        NSLog(@"perform block in thread: %d", mach_thread_self());
    });
});
dispatch_resume(timer);
複製程式碼

此時任務總是不會被處理。由於runloop需要被喚醒才能處理佇列任務,而perform block只是單純的新增任務,沒有喚醒功能。為了執行緒能夠繼續執行任務,這時候還需要不斷的wake up執行緒:

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    CFRunLoopPerformBlock(serialRunLoop, NSDefaultRunLoopMode, ^{
        NSLog(@"perform block in thread: %d", mach_thread_self());
    });
    CFRunLoopWakeUp(serialRunLoop);
});
dispatch_resume(timer);

/// 日誌輸出
2018-04-14 11:15:10.314064+0800 PThreads[19459:248764] perform block in thread: 2563
2018-04-14 11:15:10.763480+0800 PThreads[19459:248764] perform block in thread: 2563
2018-04-14 11:15:11.263651+0800 PThreads[19459:248764] perform block in thread: 2563
2018-04-14 11:15:11.763957+0800 PThreads[19459:248764] perform block in thread: 2563
2018-04-14 11:15:12.264006+0800 PThreads[19459:248764] perform block in thread: 2563
2018-04-14 11:15:12.763384+0800 PThreads[19459:248764] perform block in thread: 2563
複製程式碼

結論

從測試來看,在子執行緒啟動runloop並不是一個很明智的選擇:這會導致執行緒保活期間被遺棄,失去了處理訊息派發的能力,且無法響應其他執行緒的通訊。其次,即便可以通過perform block來繼續為保活執行緒新增任務處理,但在保活執行緒的runloop缺乏穩定的喚醒機制的情況下,還需要其他執行緒來提供喚醒能力,這增加了程式碼設計的成本,並且不會有額外的好處

關注我的公眾號獲取更新資訊

相關文章