Golang协程为什么⽐线程轻量
Golang协程(goroutine)比线程轻量,主要归因于以下几个关键因素:
一、初始堆栈大小与动态调整
- 线程:每个系统线程的堆栈大小通常很大,通常在2MB左右,且这些堆栈大小通常是固定的,不能根据需求动态改变。
- 协程:Go协程的初始堆栈大小远小于线程,只有2KB左右。更重要的是,Go协程的堆栈是动态的,可以根据需求增长和缩小。当堆栈空间不够时,Go运行时可以自动增加其大小;当堆栈空间过大时,也可以自动缩小。这种动态调整机制避免了内存浪费,使得创建大量协程所需的内存远少于创建相同数量的线程。
二、调度模型与开销
- 线程:线程的调度由操作系统负责,创建、销毁和上下文切换都需要系统调用的参与,因此开销较大。线程切换会触发用户态和内核态的切换,上下文切换的延迟较高,大约是50到100纳秒(ns)左右。
- 协程:Go协程由Go运行时而非操作系统进行调度,因此调度开销更小。Go运行时使用M:N的调度模型,其中M个goroutine可以在N个OS线程上进行调度。这使得Go可以在用户级别实现轻量级的上下文切换,避免了频繁的系统调用和线程切换。此外,Go将goroutine的调度维持在用户态,由GPM中的P(逻辑处理器)来完成调度任务,这个切换只涉及PC(程序计数器)、SP(堆栈指针)、DX(数据寄存器)三个寄存器的值的修改,相比线程的上下文切换(需要陷入内核模式以及16个寄存器的刷新)要轻量得多。
三、内存占用与资源利用
- 线程:由于线程的堆栈大小较大且固定,创建大量线程会占用大量内存资源,可能导致系统资源耗尽。
- 协程:Go协程的内存占用较小,且可以动态调整堆栈大小,因此可以创建大量的goroutines而不会过分消耗内存。这使得Go语言在处理大量并发任务时能够保持高性能和低资源消耗。
四、并发模型与通信机制
- 线程:线程通常使用共享内存的方式进行并发编程,需要开发者自己处理同步和互斥问题,容易出现竞态条件和死锁等问题。
- 协程:Go协程使用基于通信的并发模型,即通过channel进行协程之间的通信和同步。这种模型更容易实现并发安全,避免了传统线程间共享数据的竞态条件问题。此外,Go协程之间通过channel进行通信,这也是一种高效的同步机制。
综上所述,Golang协程之所以比线程轻量,是由于其初始堆栈大小小、动态调整机制、M:N调度模型、低调度开销、小内存占用、高效的并发模型和通信机制等多方面因素共同作用的结果。这些特点使得Go语言在处理大量并发任务时能够保持高性能和低资源消耗。