写在前面的话

对NSThread这个类,其实之前用的次数并不如GCD多,通常场景下,GCD已经足够应付平时开发中的线程调度了。
倘若对GCD的背景有所兴趣,想要初步了解iOS开发中的线程池,全局队列等概念,我推荐这篇文章 - 并发编程:API及挑战

可以主动阻塞的线程

在最近阅读的FFmpegTutorial中,作者编写了一个MRThread类用来扩充NSThread,用来支持join方法(原来的NSThread并不支持join方法)。
以C#(作为一门光荣且优雅的面向对象语言)为基准,稍微解释一下join是用来做什么的。

参阅MSDN的官方文档可以看到:

Blocks the calling thread until the thread represented by this instance terminates, while continuing to perform standard COM and SendMessage pumping.

—— Thread.Join Method

 

也就是说,只要join一旦被调用,且目前这个Thread对应的任务还没执行完毕,那么它就会阻塞住当前线程,直到该任务执行完毕

 

如上所示:

  • 在主线程中(为了形象,假定是在主线程中运行),thread1thread2被定义,并调用start()方法;
  • thread1执行到21行处,并输出“Current thread: Thread1”;
  • thread2执行到21行处,并输出“Current thread: Thread2”;
  • thread1中,进入if判断,由于此时处于thread1thread2已经开始运行,判断命中,调用thread2.join()thread2阻塞thread1的运行线程;
  • thread1线程被阻塞,进入“WaitSleepJoin”状态,thread2继续运行,输出“Current thread: Thread2”,“Thread1:WaitSleepJoin”,“Thread2:Running”;
  • thread2结束,thread1解除被阻塞的状态,输出“Current thread: Thread1”,“Thread1:Running”,“Thread2:Stopped”。

需要注意的是,上面阻塞的是thread1,而不是我们假定的主线程,也就是说:在哪个线程中调用,阻塞哪个
冰菓!是不是非常简单?!

于是,我们可以参考它的逻辑,用一个死循环,写一个NSThread的类似功能:

哇哦,这看上去挺不错的。
然后让我们试验一下我们的想法是否正确呢?

简单的方案

根据前面提到的想法,让我们书写一些代码扩充Thread的功能,对的,我们这次用swift(用OC也可以依葫芦画瓢实现,但是Playground实在是太方便了)。

于是代码如下:

UnitTest是我们的这次的测试类。

可以看到,我使用了extension为Thread增加了一个新方法join(),内部有一个循环,每隔10ms判断一次当前线程正在执行的任务是否已经结束。如果结束,就跳出循环,如果没有,继续以上步骤。

执行并验证一下我们的想法是否正确:

 

哇哦,可以看到:

  1. Thread1开始,并执行5秒;
  2. 第5秒,Thread2开始,此时Thread1检测到了Thread2的开始,并执行join()方法进入循环阻塞当前线程(就是Thread1);
  3. Thread2执行10秒后结束,循环跳出,Thread2结束;
  4. Thread1继续执行剩下的5秒任务;
  5. Thread1结束。

完美符合我们的预期,只要一个小小的循环,就能让Thread支持join()方法。
这项技术能让我们确保在Thread1执行完后,Thread2一定是已经执行完的状态。

更进一步的方案

怎样让Thread支持join()方法,其实还有一个更为巧妙且高级的方案。
这要运用一下ThreadRunLoop构造了。
关于RunLoop的理解,请参考:

 

在一系列的技术文章里,都描述道:在iOS应用程序的运行时态,主线程对应的RunLoop是开启的,而开发者新创建的Thread对应的RunLoop是不开启的。但是我们可以手动开启这个RunLoop。而RunLoop本身,作为一种循环结构,刷新界面(主线程),监听事件都由他负责。

接下来的原理是这样的,
一般情况下,Thread是一个一次性使用的对象,当任务结束后就会被销毁。但是当它对应的RunLoop开启后,我们就可以采取保活,处理一系列的多个任务。RunLoop的基本行为是接收事件源——处理,参考了这篇RunLoop F.A.Q.,了解到:

官方资料将 Input sources 细分成三个类别:

  1. Port-Based Sources: 对应深入理解RunLoop里的 Source1,回调函数为__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
  2. Custom Input Sources: 对应 Source0,回调函数为__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
  3. Cocoa Perform Selector Sources:就是 NSObject 的 performSelector 系列。

 

那么,我们可以开启这个线程的RunLoop(用来处理多个任务),并且指定对应Mode(只接收这个Mode的事件源),在join()方法执行的时候给予一个事件源(performSelector在刚才那个Mode并且waitUntilDone为true),那么在该线程的任务结束之后会执行这个performSelector,而直到performSelector执行完成之前,当前线程都是处于被阻塞的状态(waitUntilDone)。

那么我们可以这样书写代码:

 

如大家所见,我们先封装了一个JoinableThread(就是咱们之前说的装饰器模式!),内部的变量targetselectorarguments才是Thread真正需要的上下文。
beforeTask()方法内我们先开启了线程的RunLoop用于处理事件源。而在join()方法中执行performSelector(就相当于发送了一个事件),等待其执行完成。

让我们再次执行并验证我们的想法是否正确:

 

冰菓 x 2!
至此,以上两种方案均运作正常。

总结

本期围绕着join()方法,在Thread上进行了实验。
在一开始,使用一个循环来实现,而在PlanB中,RunLoop看上去更高级,也实行的更好。

其实笔者也不对RunLoop有着很深的了解,也没有在开发中具体用到过。通过本次阅读调查,也算是一个初步,且作为第一项应用成绩得以实施。
所以本文最大的意义在于这里:了解了RunLoop并不是一个很困难的逻辑结构,它也可以应用于非常简单低级的功能。

期待下次再见。