iOS 多线程之内的那些事情(上)
众所周知,苹果的 MAC OS X 系统以及 iOS 系统是基于三个部分的。BSD Unix,MACH 以及苹果自己开发的 IOKit 等构成了操作系统的主体,也就是 Darwin 框架。其中 MACH 主要负责的部分是 CPU 管理,内存保护以及进程间通信等等。而从 BSD Unix 那边承接过来的,基本上就是网络性能,运行效率,以及标准化的 POSIX API 这一套东西了。大多数情况下,对于 Unix 用户或者开发人员来说,POSIX API 基本上提供了多线程编程所需要的一切东西。按照 POSIX API 编写出来的多线程程序往往是最标准,而且是最容易移植的。特别是对于需要从其他平台将代码移植到 iOS 的开发人员来说,将其他多线程代码翻译到 POSIX 是最方便的。这主要也是因为 POSIX API 在事实上已经形成了一种标准,只要有多线程程序,就一定有 POSIX 的影子。
当然,对于跨平台的开发人员来说,它确实是最方便的。但是对于 iOS 的原生开发者来说,仅仅使用 POSIX API 就显得有些单薄了。特别是,当开发者把这一套固定的多线程编程的思路带到 iOS 开发中来的时候,就会遇到一些小小的问题。在这篇文章(以及今后的几篇当中)我会与大家共同探讨一下这个问题,并且对这一问题提供一些自己的看法。
事实上,在实际工作中,大多数情况下并不是特别需要用到多线程处理,这是第一个结论。一般来说,在我们的层次上,使用多线程主要还是为了解决一些用户界面交互之类的问题,比如网络之类的耗时操作,大多数时候我们会用把这种操作移到另一个线程上,以避免主界面卡住。其实归根结底以上的这些都是属于并行处理这一类,在严格的意义上说,并不是并行处理就一定是多线程。最简单的例子就要数大家都在使用的一个开源库:CocoaSocket 提供了基于 GCD 的以及基于 Runloop 的两种并行 Socket,而后者就不是线程安全的。当然,这么说或许不够准确,准确的来说,就好像大多数 Foundation 类一样,它们可以在任何线程上使用,但是如果在另外的线程上进行了访问的话就会产生错误(基本上都是 NSMutable 这一前缀的类)。事实上,基于 Runloop 的 Socket 也能很好的提供并行处理的解决方案(CFSocket 本身也是基于 Runloop 的)。NSRunloop 和 CFRunloop 是基于线程的,然而它们并不是多线程所特有的属性(请注意即便是单线程也是线程),而且有它独特的行为模式。估计我会在下一篇文章中讲到这些。
当然,对于 iOS 中多线程程序来说,最难的部分也就是 Runloop 这里。简单情况下,一个工作线程可以无限循环自己,这样就能够保证一直处在工作状态,而把时间调度之类的问题全部抛给操作系统去做。而在 iOS 环境下,这么做对系统资源的利用还不是最完美的。事实上,Runloop 相当于苹果所集成的一个关于线程睡眠与唤醒的库,每一个线程可以拥有一个 Runloop,在这种工作模式下,线程所占用的 CPU 时间最少,因为操作系统可以根据你的代码来选择何时将这个 Runloop 送入休眠状态:这个在以后会讲到。
在使用中,Foundation 类里有很多东西都与 Runloop 相关。最经常见到的就是 NSTimer 这一个类。因为在很多系统中,基于时间间隔来进行触发的事件一般都工作于一个单独的进程(大多数是系统管理的进程)上,所以大多数时候我们会误认为 NSTimer 也会与主线程达到并行的效果。而实际上,由于 NSTimer 是基于 Runloop 的,更准确的说,Timer 就是 Runloop Source 的一种。所以在同一个线程上的 NSTimer 不能达到并行的效果。这就意味着以下的一点:第一个 Timer 的代码执行完毕之前,第二个 Timer 不会被触发。这点可以很简单的通过一个内容为死循环的 Timer 进行验证。而这也等同于,在 NSTimer 的代码里添加对于多线程的同步或者说保护措施,其实是没有必要的。这样的情况只有可能在两个 Timer 被分别添加到不同的线程上才会遇到。
对于 NSTimer 来讲,还有很多有趣的其他特性。比如说要重新规划一个 NSTimer 是一件非常耗时的工作,这底下的原因也和 NSTimer 是基于 Runloop 而不是系统的线程有关。在接下来的文章里,我们就会详细的探讨一下 Runloop 这个东西,以及它的各种注意事项。