线程是一个现代计算机系统中一种广泛使用的技术,macOS/iOS 下当然也不会有什么非常特别之处,所以除了 API 有所不同以外,应该是一种比较通用的知识(实际上也是在操作系统课或者某些语言课如 Java 里会着重讲的知识)。

实际在我在接触了很长一段时间关于线程的概念和使用后,对线程还是一种很模糊的认识(不管是从使用角度还是更深入的实现角度,肯定是因为没有好好学,又被 GCD 惯坏了)。因此现在在这里从某种角度直白的总结一下:

  • 对于线程来说,首先要提到进程,进程可以理解为一个在运行的程序,它占用了从使用角度看来的一个线性连续的内存区域,其中分块存储着源程序、全局数据、栈空间、堆空间等等,总之能保证一个程序运行。
  • 接下来又说到源程序。我们写下的源程序,逻辑上应该是顺序执行的,也就是按照我们写的那么来运行,各个函数调用的顺序和使用的区域会记录在栈空间上。假如一个单线程程序,或者没有线程技术的话,程序也就是一个函数调另一个函数这样,顺顺序序的运行下去。
  • 回到线程,线程实际上就等于一个新的调用栈,比如两个线程就会有两个调用栈,也就是所谓的线程单独的栈空间。理论上它们运行起来是同时的。
  • 虽然有单独的栈空间,但线程隶属于进程,其他的内存空间都是共享的,也就是共享源程序、全局数据和堆空间。当有多个线程使用同一资源时,自然就有了各种同步加锁的问题。

还有就是「异步」这个概念,「异步」单线程也是可以做到的,但是多线程自然会带来大量的「异步」。

回过头来说 macOS/iOS 上的多线程编程,其实官方一般使用的叫法是「并发编程」,原因是 Apple 推荐使用的并不是这些「裸」线程包装,而是更高抽象的 GCD 和 NSOperation。总之是淡化「线程」这个概念,只要分发想要并发的 task,由 GCD 或者 NSOperation 来管理线程,避免由开发者带来的多线程误用或者错误。这当然是非常好的理念和技术,毕竟:

在编程里,没有什么是一层抽象不能解决的,如果有,就再抽象一层。

虽然有 GCD 这样让开发者美滋滋的好东西,但我们还是要了解一下线程不是?

线程接口概览

  • Mach 线程

    XNU 内核以一个被深度定制的 Mach 3.0 内核作为基础。

    macOS/iOS 使用 Mach 作为其内核,所以线程是在这里实现的,当然就会提供线程的接口,但是这一层的接口肯定是与其他平台不一样的(比如 Linux)。

  • POSIX 线程

    POSIX 线程其实就是 pthread。很明显,因为它是 POSIX,所以它是一套在各个平台都相同的标准接口。从程序员的角度看,它就是一套 C 语言线程库。在 macOS/iOS 平台下,其实现肯定是对 Mach 线程的包装。

  • NSThread

    NSThread(Swift 中的 Thread)是 Foundation 框架里对于线程的表示,相比 pthread 来说是更高层的抽象,面向对象的封装。NSThread 据说比 pthread 还要早出现,以前应该是直接封装的 Mach 线程,但是从现在(macOS 10.12, Xcode 8.3)的调用栈来看,有可能是对 pthread 的封装:

接下来大概记录下 NSThread,也就是 Foundation 层的使用(到处都有文章,就随便记录一下)

创建线程

  1. 使用 +detachNewThreadSelector:toTarget:withObject: 或者 macOS 10.12/iOS 10 新加的方法 +detachNewThreadWithBlock: 创建线程,调用完线程就跑起来了。
  2. 使用 -initWithTarget:selector:object: 方法或者 -initWithBlock: 方法创建一个线程对象,再调用 -start 方法让它跑起来。
  3. 继承 NSThread,重写 -main 方法作为入口。使用 -init 方法实例化对象,再使用 -start 方法启动线程。
  4. 使用 NSThreadNSObject 加上的 category 中的 -performSelectorInBackground:withObject: 方法。

同步

文档都说的很清楚,就不复述了:

  • NSLocking
  • NSLock
  • @synchronized
  • NSRecursiveLock
  • NSConditionLock
  • NSDistributedLock
  • NSCondition

自动释放池

在 AppKit 和 UIKit 应用中,NSApplication 或者 UIApplication 会帮我们在主线程设置好自动释放池,但在新建的线程和 Foundation 应用(命令行)中,需要自己手动新建自动释放池。自动释放池是与线程绑定的。

上面这段话很正确,但是现在的自动释放池的实现中,即使没有显式的创建自动释放池,还是可以正常的使用 autorelease,在线程销毁时会对 autorelease 的对象发送 release 消息,但是:

Explicit is better than implicit.

所以还是显式的创建一下比较好。

Run Loop

Run loop 也就是 NSRunLoop 或者 CFRunLoop,是事件驱动编程event loop 在 macOS/iOS 平台下的实际实现,提供了接受事件、分发事件和在没有事件时睡眠线程等功能。

Run loop 也是每个线程独立的,通过 NSRunLoop+currentRunLoop 或者 CFRunLoopGetCurrent 函数就可以获取当前线程的 run loop,没有的话则会创建。在 run loop 创建之后,run loop 就能接收事件。

Run loop 开启后,需要主动调用它的方法使它跑起来,官方文档中推荐的用法如下所示:

- (void)skeletonThreadMain 
{
    // Set up an autorelease pool here if not using garbage collection.
    BOOL done = NO;
 
    // Add your sources or timers to the run loop and do any other setup.
 
    do
    {
        // Start the run loop but return after each source is handled.
        SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
 
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;
 
        // Check for any other exit conditions here and set the
        // done variable as needed.
    }
    while (!done);
 
    // Clean up code here. Be sure to release any allocated autorelease pools.
}

总之并不是使用 run loop 来保持线程不退出,而是自己建立循环,检查退出条件,使用 run loop 来处理事件。所以官方并不是很推荐使用 -run 这个不退出的方法。

Run loop 足够再写一篇文章记录,在这里就不多写了。对于 macOS/iOS 中带有 UI 的应用开发,run loop 是必不可少的。

参考