控制线程(NSThread)和运行时循环(NSRunLoop)的退出

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


这是讲iOS的线程的文章,下面的内容,自己都惨不忍睹啊,哈哈,练习翻译一下文章,英语太差啊,尽量止步吧。。。。

--------------------------------分割线--------------------------------------------------------------------


很多时候处理多线程并发的情况时,GCD已经能很好满足我们的需求,由GCD来管理线程,我们只要设定处理的任务即可。调用者不必亲自管理某个线程。但是有时候我们需要用到NSThread,需要亲自维护一个线程对象,来处理一些特殊的情况和需求。比如,当处理大型网络并发开发编程时,用GCD是一个相当有用和明智的决定。你也可以考虑用这个流行的 CocoaAsyncSocket库,这个库在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];
  }
}

保证线程资源的销毁

这段代码这里还有另一个问题:当线程被通知退出的时候,自动释放池还没有释放池里的资源。假如不能保证线程的内存资源被释放,那么我们线程同步的工作就没什么吸引了了。
但是这里有一点矛盾。NSCondition makes no promise that it is free from using -autorelease in 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,这里可以下载例子代码








你可能感兴趣的:(iOS)