写在前面的话
对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.
也就是说,只要join一旦被调用,且目前这个Thread对应的任务还没执行完毕,那么它就会阻塞住当前线程,直到该任务执行完毕。
如上所示:
- 在主线程中(为了形象,假定是在主线程中运行),thread1和thread2被定义,并调用start()方法;
- thread1执行到21行处,并输出“Current thread: Thread1”;
- thread2执行到21行处,并输出“Current thread: Thread2”;
- 在thread1中,进入if判断,由于此时处于thread1且thread2已经开始运行,判断命中,调用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判断一次当前线程正在执行的任务是否已经结束。如果结束,就跳出循环,如果没有,继续以上步骤。
执行并验证一下我们的想法是否正确:
哇哦,可以看到:
- Thread1开始,并执行5秒;
- 第5秒,Thread2开始,此时Thread1检测到了Thread2的开始,并执行join()方法进入循环阻塞当前线程(就是Thread1);
- Thread2执行10秒后结束,循环跳出,Thread2结束;
- Thread1继续执行剩下的5秒任务;
- Thread1结束。
完美符合我们的预期,只要一个小小的循环,就能让Thread支持join()方法。
这项技术能让我们确保在Thread1执行完后,Thread2一定是已经执行完的状态。
更进一步的方案
怎样让Thread支持join()方法,其实还有一个更为巧妙且高级的方案。
这要运用一下Thread的RunLoop构造了。
关于RunLoop的理解,请参考:
在一系列的技术文章里,都描述道:在iOS应用程序的运行时态,主线程对应的RunLoop是开启的,而开发者新创建的Thread对应的RunLoop是不开启的。但是我们可以手动开启这个RunLoop。而RunLoop本身,作为一种循环结构,刷新界面(主线程),监听事件都由他负责。
接下来的原理是这样的,
一般情况下,Thread是一个一次性使用的对象,当任务结束后就会被销毁。但是当它对应的RunLoop开启后,我们就可以采取保活,处理一系列的多个任务。RunLoop的基本行为是接收事件源——处理,参考了这篇RunLoop F.A.Q.,了解到:
官方资料将 Input sources 细分成三个类别:
- Port-Based Sources: 对应深入理解RunLoop里的 Source1,回调函数为
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
- Custom Input Sources: 对应 Source0,回调函数为
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
- Cocoa Perform Selector Sources:就是 NSObject 的 performSelector 系列。
那么,我们可以开启这个线程的RunLoop(用来处理多个任务),并且指定对应Mode(只接收这个Mode的事件源),在join()方法执行的时候给予一个事件源(performSelector在刚才那个Mode并且waitUntilDone为true),那么在该线程的任务结束之后会执行这个performSelector,而直到performSelector执行完成之前,当前线程都是处于被阻塞的状态(waitUntilDone)。
那么我们可以这样书写代码:
如大家所见,我们先封装了一个JoinableThread(就是咱们之前说的装饰器模式!),内部的变量target,selector和arguments才是Thread真正需要的上下文。
在beforeTask()方法内我们先开启了线程的RunLoop用于处理事件源。而在join()方法中执行performSelector(就相当于发送了一个事件),等待其执行完成。
让我们再次执行并验证我们的想法是否正确:
冰菓 x 2!
至此,以上两种方案均运作正常。
总结
本期围绕着join()方法,在Thread上进行了实验。
在一开始,使用一个循环来实现,而在PlanB中,RunLoop看上去更高级,也实行的更好。
其实笔者也不对RunLoop有着很深的了解,也没有在开发中具体用到过。通过本次阅读调查,也算是一个初步,且作为第一项应用成绩得以实施。
所以本文最大的意义在于这里:了解了RunLoop并不是一个很困难的逻辑结构,它也可以应用于非常简单低级的功能。
期待下次再见。
评论