系统线程和goroutine的区别

本文对比了Go语言中的goroutine与操作系统线程在栈空间、调度开销和标识方面的区别。goroutine具有动态调整栈空间的能力,调度开销小,且没有可供程序员使用的标识,旨在提供更高效、简洁的并发编程体验。

从栈空间上,goroutine的栈空间更加动态灵活。

每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存线程执行期间的局部变量,且大小是固定不变的,在多变的场景下,这样固定大小的栈,既太大,又太小,往往不能满足多变的场景。
2MB固定大小的栈,对于执行简单操作的goroutine来说,是一种巨大的浪费;但对于执行高度复杂的goroutine来说,又太过于小了。为了适应不同场景,goroutine在生命周期开始时只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。

从调度上看,goroutine的调度开销远远小于线程调度开销。

OS的线程由OS内核调度,每隔几毫秒,硬件就发送中断到CPU,CPU调用调度器内核函数,进而引发当前线程的暂停和下个线程的运行,而线程与线程之间的切换,需要一个完整的上下文切换。由于引发内存访问数量的增加和CPU等待周期的增加,这一操作是非常耗时的。
但是Go运行的时候包含一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,把m个goroutine调度到n个os线程上运行,我们可以用GOMAXPROCS来控制n的数量,Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,全部在用户态实现,不需要切换到内核语境,因此调度一个goroutine比调度一个线程的成本低很多。

goroutine没有标识

在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。
goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还要取决于运行它的线程标识,这就造成了函数的行为变得诡异莫测,与Go语言所鼓励的简单的编程风格不匹配。

参考:

### Go语言中的Goroutine概念 GoroutineGo语言中的一种并发执行机制,它允许开发者以非常低的成本创建大量的并发任务。Goroutine可以被视为轻量级的线程,但与传统的线程不同,它的管理调度是由Go运行时(runtime)负责的,而不是操作系统[^1]。 Goroutine的设计目的是为了简化并发编程,使得开发者能够更容易地编写出高效、可扩展的应用程序。每个Goroutine都有自己的栈空间,初始大小通常为4KB,并且可以根据需要动态增长或缩小[^3]。这种轻量级的设计意味着一个Go程序可以同时运行成千上万个Goroutine而不会消耗过多的内存资源。 ### Goroutine线程区别 #### 调度机制 线程调度通常由操作系统内核完成,而Goroutine则是由Go运行时进行调度。这意味着Goroutine之间的切换不需要进入内核态,因此上下文切换的成本远低于线程[^4]。此外,Go运行时会智能地将Goroutine分配给可用的CPU核心,从而实现高效的并行处理。 #### 栈空间 线程在创建时通常会分配较大的固定大小的栈空间,比如默认情况下Linux系统上的线程栈大小可能是8MB左右。相比之下,Goroutine的初始栈大小要小得多,大约为2KB到4KB之间,并且可以根据需要自动增长缩小[^3]。这使得Goroutine更加节省内存资源。 #### 标识符 线程通常拥有一个全局唯一的标识符,可以通过系统调用来获取操作。然而,Goroutine并没有提供类似的全局唯一标识符机制。尽管可以通过某些方式给Goroutine赋予一个唯一的ID,但这并不是语言本身的标准特性[^2]。 #### 创建销毁成本 由于线程的创建销毁涉及到操作系统级别的资源分配释放,因此其开销较大。而Goroutine的创建则非常迅速且占用较少的资源,因为它们是在用户空间中由Go运行时管理的。同样地,销毁Goroutine的成本也比销毁线程要低得多[^1]。 #### 通信同步 线程间的通信同步通常需要借助于锁、信号量等机制来保证数据的一致性,这可能导致复杂的编程模型以及潜在的死锁问题。Goroutine支持通过channel来进行通信,这是一种更高级别的抽象,有助于避免许多常见的并发问题,如竞态条件死锁等[^4]。 下面是一个简单的Go代码示例,演示了如何启动两个Goroutine并通过channel进行通信: ```go package main import ( "fmt" "time" ) func say(s string, c chan<- string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } c <- "done" } func main() { c := make(chan string) go say("world", c) go say("hello", c) // Wait for both goroutines to finish fmt.Println(<-c) fmt.Println(<-c) } ``` 在这个例子中,`say`函数被作为Goroutine启动两次,分别打印"world""hello"。主函数中的channel用于等待两个Goroutine都完成它们的工作。 ### 总结 GoroutineGo语言提供的一个强大的并发编程工具,它通过减少并发任务的创建成本优化调度策略,使得编写高并发的应用程序变得更加简单高效。与传统线程相比,Goroutine不仅在资源消耗上更为经济,而且提供了更加简洁的API来进行并发控制通信。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值