1. 程式人生 > >控制執行緒(NSThread)和執行時迴圈(NSRunLoop)的退出

控制執行緒(NSThread)和執行時迴圈(NSRunLoop)的退出

原文地址:http://shaheengandhi.com/controlling-thread-exit/

這是講iOS的執行緒的文章,下面的內容,自己都慘不忍睹啊,哈哈,練習翻譯一下文章,英語太差啊,儘量止步吧。。。。

--------------------------------分割線--------------------------------------------------------------------

很多時候處理多執行緒併發的情況時,GCD已經能很好滿足我們的需求,由GCD來管理執行緒,我們只要設定處理的任務即可。呼叫者不必親自管理某個執行緒。但是有時候我們需要用到NSThread,需要親自維護一個執行緒物件,來處理一些特殊的情況和需求。比如,當處理大型網路併發開發程式設計時,用GCD是一個相當有用和明智的決定。你也可以考慮用這個流行的庫,這個庫在iOS上很好的抽象和封裝了複雜的socket程式設計。但是,本文存在意義,就是假設某個工作需要一個單獨的執行緒(不使用GCD),而且我們可以清楚地開始和停止這個執行緒。所謂清楚的開始和停止這個執行緒,指的就是當我們決定要開始或者停止這執行緒時,我們必須保證這個執行緒做了初始化或者銷燬工作。 你可以從
github
得到這篇文章的完整例子。 啟動執行緒 請記住,執行緒是屬於作業系統的資源,並不是objective-c語言執行時的特性。那就意味著那些很有用的類,例如,NSAutoreleasePool 和 an NSRunLoop也需要被被建立執行緒的那段程式碼管理。這裡有一個程式碼片段,是關於設定自動釋放池(autorelease pool)和啟動執行時迴圈(NSRunLoop)的。
- (void)start
{
  if (_thread) {
    return;
  }

  _thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadProc:) object:nil];
}

- (void)threadProc:(id)ignored
{
  @autoreleasepool {
    // Startup code here

    // Just spin in a tight loop running the runloop.
    do {
      [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]
    } while (TRUE);
  }
}
當然,這段程式碼有很多問題。可以看到這個執行時迴圈是每隔1秒執行的。這個執行緒並沒做什麼事,當超時以後,這個執行緒就會每隔1秒被喚醒,執行while這個迴圈的判斷語句。這個是很費cpu和電池的。當然這是個死迴圈,執行緒也是無法退出的。即使我們希望這個執行緒的生命週期能跟我們的程序的生命週期一樣,我們也是需要找到合適的方法退出執行緒和清理相關的資源。這裡枚舉了幾個需要修復的問題: 1.無法保障這個執行緒是否可以正常執行工作。 2.這個執行緒從來不會進入休眠狀態 3.無法退出執行緒和做相關的資源清理。 一下子解決這3個問題是不容易的,修復第二個問題最好就是,平時讓執行緒進入掛起(休眠)狀態,只有當有工作的時候才需要被喚醒(可以被runloop的輸入源喚醒)。但是,這就導致很難近視退出這個執行緒。
執行緒無限等待 我們怎麼樣才能讓執行時迴圈(NSRunLoop)無限等待呢?我們的目的就是讓執行緒進入休眠狀態。檢視蘋果的官方文件,對於這個引數的描述runUntilDate:這個方法並不是我們想要的。 If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate: until the specified expiration date. 第二句話說明一直會在CPU上自旋,這跟我們的需求是截然想反的。更好的方案是呼叫runMode:beforeDate:這個方法的文件說明:
If no input sources or timers are attached to the run loop, this method exits immediately and returns NO; otherwise, it returns after either the first input source is processed or limitDate is reached. 至少當呼叫這個方法的時候,執行緒還有機會進入休眠狀態。然而,這個執行緒還是會進入死迴圈因為沒有輸入源或者定時器。如果你只是想在執行時迴圈裡呼叫performSelector: ,你需要給執行時迴圈新增一個你自己的輸入源,讓執行緒進入休眠狀態。使用輸入源給執行緒傳送任務是非常有效地,但這個練習就留給讀者了。 最後一件事,方法裡的引數NSDate,我們應該使用什麼值呢?任意一個非常大的值都是合適的,讓執行緒隔一天醒一次足夠讓執行緒保持掛起狀態。+[NSDate distantFuture]是一個很方便的工廠方法來獲得這樣一個很大的數值。
static void DoNothingRunLoopCallback(void *info)
{
}

- (void)threadProc:(id)ignored
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    do {
      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                               beforeDate:[NSDate distantFuture]];
    } while (TRUE);

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
  }
}
退出執行緒的條件 如上述的程式碼所示,這個執行緒就永遠進入了休眠狀態,我們怎麼來保證關閉這個執行緒呢?這完全有可能使用來+[NSThread exit]殺死當前執行的執行緒。但是這樣暴力的方法並沒有清理棧上對堆物件的引用,沒有釋放資源,例如runloop source ,而且頂層的那個自動釋放池也不能釋放自動釋放的物件,也不能讓執行緒清理和處理剩餘的工作。所以我們應該然執行緒停止休眠狀態,能退出這個方法runMode:beforeDate:而且我們需要一個條件,能讓執行緒進行判斷是時候關閉自己了(退出執行緒)。 NSRunLoop的runMode:beforeDate:的退出條件多少有幾分限制。這個方法返回YES(當runloop處理了一個輸入源或者設定的時間超時了)或者NO(當runloop不能被開啟)沒什麼用。根據文件所說的,這個方法只有當沒有輸入源或者定時器的時候才會返回NO,但是我們的程式碼永遠不會返回NO,因為為了讓runloop進入迴圈狀態,我們已經給它添加了一個輸入源了。 很幸運,NSRunLoop封裝了CFRunLoop這些API。CoreFoundation提供了CFRunLoopRunInMode可供我們選擇,它提供了一個更專業的方法來退出runloop。很明確的,kCFRunLoopRunStopped說明runloop可以被這個方法停止CFRunLoopStop這也是CFRunLoopRun可以退出的原因(除了沒有輸入源和定時器,但這種情況不會出現,因為我們有假的輸入源),我們也不需要為CFRunLoopRunInMode操心,也不需要檢查條件。 最好在目標執行緒上執行CFRunLoopStop。我們可以這樣使用: performSelector:onThread:withObject:waitUntilDone:. 所以新的執行緒函式threadProc:,看起來像這樣的:
- (void)threadProc:(id)ignored
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
  }
}
我們可以像下面的程式碼一樣在任何其他執行緒裡退出這目標執行緒,包括目標執行緒它自己。
- (void)stop
{
  [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
  _thread = nil;
}

- (void)_stop
{
  CFRunLoopStop(CFRunLoopGetCurrent());
}
備註:_thread指的是目標執行緒,開啟了runloop 的執行緒。 同步執行緒的啟動和退出 至少還有兩個問題需要解決,當啟動執行緒的時候,我們怎麼保證它已經就緒了呢?當關閉執行緒的時候,我們能保證它被銷燬了嗎? 我認為有更好地執行緒模式來嘗試保證目標執行緒的狀態。舉個例子,一個執行緒應該接受任務,但是目標執行緒還沒就緒,就不應該這行處理任務。Resources outside of the thread's runloop should be minimal such that ensuring the thread is no longer executing should be above and beyond the desired knowledge of the thread's state.(不是很理解,不知道怎麼翻譯了)。 有一種很誘人和簡單的方法,把performSelector:<(SEL)onThread:(NSThread *) withObject:(id)waitUntilDone:(BOOL)裡的waitUntilDone設定成YES,讓它來等待目標執行緒已經退出,但是這個方法只會等待_stop方法的結束,並不會等待目標執行緒的清理工作結束。為了能讓它等待目標執行緒清理工作結束,我們需要做一個新的假設:目標執行緒是被其他執行緒關閉的。因為這是不可能的,讓目標執行緒等待被自己關閉。 為了讓目標執行緒通知主調執行緒(控制關閉目標執行緒的執行緒)它已經結束了,一定要在他們之間共享一個變數。NSCondition 提供了方便的方法,來達到我們的目的。 這個執行緒的管理程式碼如下。這種模式讓執行緒長時間進入休眠狀態當沒有任務執行那個的時候,並且可以執行緒快速地響應退出和做清理工作。而且支援同步的啟動和關閉執行緒。
- (void)start
{
  if (_thread) {
    return;
  }

  _thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadProc:) object:nil];

  // _condition was created in -init
  [_condition lock];
  [_thread start];
  [_condition wait];
  [_condition unlock];
}

- (void)stop
{
  if (!_thread) {
    return;
  }

  [_condition lock];
  [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
  [_condition wait];
  [_condition unlock];
  _thread = nil;
}

- (void)threadProc:(id)object
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    [_condition lock];
    [_condition signal];
    [_condition unlock];

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);

    [_condition lock];
    [_condition signal];
    [_condition unlock];
  }
}
保證執行緒資源的銷燬 這段程式碼這裡還有另一個問題:當執行緒被通知退出的時候,自動釋放池還沒有釋放池裡的資源。假如不能保證執行緒的記憶體資源被釋放,那麼我們執行緒同步的工作就沒什麼吸引了了。 但是這裡有一點矛盾。NSConditionmakes no promise that it is free from using-autoreleasein its implementations of-lock,-signal, and-unlock. 那就意味著應該有一個有效的NSAutoreleasePool,當使用這些API的時候。我們有兩個解決方案。我們可以手動執行自動釋放池,或者使用另一種同步方式來同步執行緒的退出。第一種方式有點雜亂。第二種方式有太多的變數。 手動執行自動釋放
為了直接使用NSAutoreleasePool,你必須關閉ARC。 記住使用-[NSAutoreleasePool drain],跟-[NSAutoreleasePool release]一樣有效,當我們釋放後自動釋放池就不再有效了。所以,手動釋放釋放池就意味著新建另一個自動釋放池來保證NSCondition的API有正確的環境。
- (void)threadProc:(id)object
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    [_condition lock];
    [_condition signal];
    [_condition unlock];

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);

    // Release all accumulated resources, but make sure NSCondition has the
    // right environment.
    [pool drain];
    pool = [[NSAutoreleasePool alloc] init];

    [_condition lock];
    [_condition signal];
    [_condition unlock];
  }
  [pool drain];
}
使用NSThreadWillExitNotification 當執行緒的主函式執行完畢和當執行緒將要執行完畢的時候,NSThread會發出NSThreadWillExitNotification這個通知。這個通知是在threadProc:以後,所以可以保證自動釋放池已經釋放了。儘管這個通知是有退出的執行緒傳送的,NSCondition任然可以同步現成的狀態。
- (void)stop
{
  if (!_thread) {
    return;
  }

  NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  [nc addObserver:self selector:@(_signal) name:NSThreadWillExitNotification object:_thread];

  [_condition lock];
  [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
  [_condition wait];
  [_condition unlock];

  [nc removeObserver:self name:NSThreadWillExitNotification object:_thread];
  _thread = nil;
}

- (void)threadProc:(id)object
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    [_condition lock];
    [_condition signal];
    [_condition unlock];

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
  }
}

- (void)_signal
{
  [_condition lock];
  [_condition signal];
  [_condition unlock];
}
使用pthreads 上述的所有的解決方案都有一個小問題:這個執行緒還沒有完全退出,然控制執行緒認為目標執行緒已經退出了。目標執行緒快要結束,但是還是沒有真正的結束。
所以我們要做出NSThread的範圍,使用更低層的pthreads。pthread_join能保證執行緒完全退出了。使用pthreads讓程式碼更加冗長了,而且一些記憶體管理要更加小心。當使用NSthread的初始化方法時,self是會被作為引數增加一次引用計數的。但是使用pthread_create是不會增加引用計數的。注意到這點,我們仍然需要引用NSThread的物件來執行performSelector:onThread:withObject:waitUntilDone:,但是沒有方法從pthread_t轉化成NSThread。但是,很幸運,+[NSThread currentThread]可以獲取當前物件的引用。 NSCondition 仍然可以用作啟動的同步方案。因為它沒被用作其他用處,不是必須加鎖線上程啟動之前。但是為了與之前的程式碼保持一致性,我們會遵循之前的模式,當新建一個執行緒的時候,讓他保持掛起狀態,當它獲得了條件鎖的時候在恢復執行狀態。
static void *ThreadProc(void *arg)
{
  ThreadedComponent *component = (__bridge_transfer ThreadedComponent *)arg;
  [component threadProc:nil];
  return 0;
}

- (void)start
{
  if (_thread) {
    return;
  }

  if (pthread_create_suspended_np(&_pthread, NULL, &ThreadProc, (__bridge_retained void *)self) != 0) {
    return;
  }

  // _condition was created in -init
  [_condition lock];
  mach_port_t mach_thread = pthread_mach_thread_np(_pthread);
  thread_resume(mach_thread);
  [_condition wait];
  [_condition unlock];
}

- (void)stop
{
  if (!_thread) {
    return;
  }

  [self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
  pthread_join(_pthread, NULL);
  _thread = nil;
}

- (void)threadProc:(id)object
{
  @autoreleasepool {
    CFRunLoopSourceContext context = {0};
    context.perform = DoNothingRunLoopCallback;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);

    // Obtain the current NSThread before signaling startup is complete.
    _thread = [NSThread currentThread];

    [_condition lock];
    [_condition signal];
    [_condition unlock];

    // Keep processing events until the runloop is stopped.
    CFRunLoopRun();

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
  }
}

程式碼例子 github,這裡可以下載例子程式碼