Skip to content

多线程

多线程并发编程是充分利用现代处理器性能的重要课题。在此处我们暂不讨论内核态线程(运行在内核内部的线程,由内核直接管理,用户态无法访问),仅讨论用户态线程。

多个线程位于同一个进程中,共享进程的资源,例如:内存、网络等等;同时,让多个函数并行执行,提高程序的运算能力。其中,并发访问共享内存会导致出乎意料的程序错误,为此,必须谨慎处理,并使用一定的技巧。

线程模型

用户态的线程往往基于内核线程来进行封装。一个用户线程可以封装一个内核线程,或者多个用户态线程映射多个内核线程。

  • 一对一:经典封装,直接封装操作系统提供的线程进行使用
  • 多对一:在使用单一系统线程的情况下,呈现并行假象,用于减少阻塞
  • 多对多:M:N 模型,使用复杂的用户太调度器来极致压榨性能

因此,用户态线程根据抽象层次可以分为:应用级线程和系统级线程。

一对一模型往往是系统级线程,在程序中创建的一个线程就是对一个系统线程的封装,而多对一和多对多的模型往往是应用级线程。我们使用的时候往往是一个应用级线程库调度的线程对象。

系统级线程

系统级线程往往就是对系统调用的封装,一个应用中的线程对应的就是操作系统提供的线程,常见的系统级线程包括:

  • C:posix thread 库
  • Java:标准库 Thread

应用级线程(纤程、协程)

应用级线程是指完全由用户空间的线程库管理和调度的线程,与系统级线程不同,用户态线程的创建、销毁、同步和调度等操作都不需要内核的直接参与,因此具有更小的开销和更高的灵活性。用户态线程的切换只涉及用户空间的上下文切换,不会引发内核态切换,因此速度更快。纤程往往是多个实例共享一个底层的系统线程,让 CPU 能够充分使用,防止阻塞。

有栈(stack-based):

  • C: posix ucontext, M:1
  • Go: goroutine, M:N

无栈(stack-free,基于状态机):

  • C#: async/await, M:N
  • Rust: async/await, M:N
  • JavaScript: async/await, M:1
  • Python: async/await, M:1

用户态线程适合于大量并发、轻量级任务的场景,但由于内核并不感知用户态线程的存在,所以当一个用户态线程阻塞(如I/O操作)时,整个进程都会被阻塞,无法充分利用多核CPU资源。

在异步编程中,一个可以用户主动中断程序执行,并在未来的某个阶段从中断点恢复。并使用了 async/await 语法来封装跳转的动作,优化程序员的开发体验。

有栈实现和无栈实现

有栈实现通过维护一个用户态的栈空间,模仿操作系统为线程抽象的一个栈空间,从而来模拟用户空间的线程,运行时将函数中的状态保存在栈上,在调用时一并使用栈空间中的状态,从而保存状态;

无栈实现使用更加激进的手段,将一个个 async 函数转化为一个同步的状态机函数和一个状态对象,状态对象中保存着当前函数运行到的中断点,同时并由外部调度器监听事件,并在合适的时机进行调度。