Go语言入门与进阶:常见的线程模型

这是我参与更文挑战的第 25 天,活动详情查看: 更文挑战

前文回顾

如果你还没有 Go 语言基础,建议阅读我的 从零学 Go

本系列文章,我将会进一步加深对 Go 语言的讲解,更一步介绍 Go 中的包管理、反射和并发等高级特性。

前面一篇文章主要介绍了 Go 语言的并发模型相关的概念。线程之间的调度永远是一个复杂的话题,但是并发编程必然会涉及到操作系统对线程的调度。本文将会具体介绍常见的线程模型。

常见的线程模型

根据资源访问权限的不同,操作系统会把内存分为内核空间和用户空间,内核空间的指令代码具备直接调度计算机底层资源的能力,比如说 I/O 资源等;用户空间的代码没有访问计算底层资源的能力,需要通过系统调用等方式切换为内核态来实现对计算机底层资源的申请和调度。

线程作为操作系统能够调度的最小单位,也分为用户线程和内核线程:

  • 用户线程由用户空间的代码创建、管理和销毁,线程的调度由用户空间的线程库完成(可能是编程语言层次的线程库),无需切换内核态,资源消耗少且高效。对 CPU 的竞争是以所属进程的维度参与的,同一进程下的所有用户级线程只能分时复用进程被分配的 CPU 时间片,所以无法很好利用 CPU 多核运算的优势。我们一般情况下说的线程其实是指用户线程;
  • 内核线程由操作系统管理和调度,能够直接操作计算机底层的资源,线程切换的时候 CPU 需要切换到内核态。它能够很好利用多核 CPU 并行计算的优势,开发人员可以通过系统调用使用内核线程。

用户级线程模型

用户级线程模型中基本是一个进程对应一个内核线程,如下图所示:

用户级线程模型.png

进程内的多线程管理由用户代码完成,这使得线程的创建、切换和同步等工作显得异常轻量级和高效,但是这些复杂的逻辑需要在用户代码中实现,一般依赖于编程语言层次。同时进程内的多线程无法很好利用 CPU 多核进程的优势,只能通过分时复用的方式轮换执行。当进程内的任意进程阻塞,比如线程 A 请求 I/O 操作被阻塞,很可能导致整个进程范围内的阻塞,因为此时进程对应内核线程因为线程 A 的I/O 阻塞而被剥夺 CPU 执行时间,导致整个进程失去了在 CPU 执行代码的权利!

内核级线程模型

内核级线程模型中,进程中的每个线程都会对应一个内核线程,如图所示:

内核级线程模型.png

进程内每创建一个新的线程都会调用操作系统的线程库在内核创建一个新的内核线程与对应,线程的管理和调度有操作系统负责,这将导致每次线程切换上下文时都会从用户态切换到内核态,会有不小的资源消耗,同时创建线程的数量也会受制于操作系统内核创建可创建的内核线程数量。好处是多线程能够充分利用 CPU 的多核并行计算能力,因为每个线程可以独立被操作系统调度分配到 CPU 上执行指令,同时某个线程的阻塞并不会影响到进程内其他线程工作的执行。

两级线程模型

两级线程模型相当于用户级线程模式和内核级线程模型的结合,一个进程将会对应多个内核线程,由进程内的调度器决定进程内的线程如何与申请的内核线程对应,如图所示:

两级线程模型.png

进程会预先申请一定数量的内核线程,然后将自身创建的线程与内核进程进行对应。线程的调用和管理由进程内的调度器进行,而内核线程的调度和管理由操作系统负责。这种线程模型即能够有效降低线程创建和管理的资源消耗,也能够很好提供线程并行计算的能力,但是给开发人员带来较大的实现难度。

小结

本文主要介绍了 Go 语言常见的线程模型,包括用户线程和内核线程、两级线程模型。用户线程是无法被操作系统感知的,用户线程所属的进程或者内核线程才能被操作系统直接调度,分配 CPU 的使用时间。对此衍生出了不同的线程模型,它们之间对 CPU 资源的使用程度各有千秋。

阅读最新文章,关注公众号:aoho求索