线程实现模型(Thread Implementation models)

前言

在之前,我总结了一篇用户级线程 与 内核级线程的问答,描述了用户级线程内核级线程各自的特性。这也使我一度认为,Python中常常被人诟病的多线程无法同时被多核分配的原因,是因为它使用了用户级线程。然而最近,在我重新审视一些多线程的问题时,我突然发现,我的理解存在很大偏差。于是我重新查阅了一些资料,在这里重新进行梳理。

相关概念

POSIX Threads

为了保证多线程编程的可移植性,IEEE定义了一套标准的线程接口规范,这就是POSIX线程规范。跟据这个规范实现的线程库,通常被称作Pthread。POSIX Threads只是接口规范的定义,具体的实现取决于不同的操作系统。

KSE(Kernel Scheduling Entity)

内核调度实体是最终内核进行调度的对象,可以将线程或者进程看作是基于KSE构建的抽象调度单元,仅仅是在共享的一些属性上有些或多或少的差异(虚拟内存、文件描述符、PID等等)。Posix Threads对线程间属性共享的要求,只是众多可能性中的一种。

线程实现模型(Thread Implementation models)

不同线程实现模型间的主要区别在于线程与KSEs的映射关系。

多对一实现(Many-to-one M:1 Implementations)

多对一实现,又被称作用户级线程(user-level threads)。
在多对一实现中,线程的创建、调度和同步完全由用户空间的线程库来处理,内核对多个线程的存在并无感知。那么,这样的实现就存在两个问题:

  • 当一个线程发起系统调用,比如read(),控制权由用户空间的线程库转交到了内核,那么所有线程都将被阻塞

  • 由于内核感知不到用户空间多个线程的存在,内核无法参与调度,线程也就无法被多核分配。

第二点听起来很像Python多线程的情况,但实际并不是这样,这也是我最开始理解错的地方。线程实现取决于操作系统,Linux中的线程是使用1:1模型实现的,而Python多线程同一时刻只有一个线程运行,是因为Python解释器存在线程安全问题,引入了GIL,从而限制了多个线程的调度运行

一对一实现(One-to-one 1:1 Implementations)

一对一实现,又被称作内核级线程(kernel-level threads)。
在一对一实现中,每个线程对应一个独立的KSE。内核可以独立调度每一个线程,线程间同步是通过系统调用实现的。
1:1实现解决了一个M:1中的问题。一个阻塞的系统调用不会导致进程中所有线程都被阻塞。然而,内核需要为每一个线程维护一个KSE,如果开启了大量的线程,会增加内核调度的负荷,影响系统性能。

尽管存在着一些缺点,但1:1模型仍然是比较好的选择。Linux系统的两种线程实现 —— LinuxThreads 和 NPTL(Native Posix Threads Library) 都是使用的1:1实现模型。

多对一实现(Many-to-many M:N Implementations)

多对多实现,又被称作两级线程(two-level threads)。
两级线程实现解决了M:1和1:1实现模型的缺点,然而它也存在一个重要的问题,就是它两级实现过于复杂(complexity)了。
起初NPTL考虑过M:N实现,但是由于需要对内核进行修改,而且可能并没有足够的必要性,最终被驳回了。

总结

综上所述,线程实现取决于操作系统,Linux选用的是1:1实现模型,即内核级线程。
而Python在Linux平台上同样使用的是Linux pthread,然而由于GIL,导致了线程被限制,无法同时被多核调度。

参考

  1. The Linux Programming Interface 28.2.1/33.4
  2. Linux Man Pages - pthread
  3. 现代操作系统