为什么Go在并发方面超越了所有对手?
\n\n想象一位同时烹饪十道菜的大厨。他并不是在炉灶之间疯狂奔跑——他只是高效地切换任务:炖肉的时候切沙拉;水烧开的时候摆盘。在编程世界里,这被称为并发——一种无需等待每个任务完成就能管理多个任务的能力。
\n\nGo语言(Golang)正是为解决并发问题而生的。它的Google创建者们厌倦了C++和Java中多线程的复杂性。他们发明了goroutine——一种轻量级线程,只需一行代码即可启动。在本文中,我们将深入探讨Go中的并发工作原理,将其与操作系统线程以及Python的async/await范式进行比较,更重要的是,我们将展示您今天就能使用的真实代码示例。
\n\nGoroutine vs 操作系统线程:轻骑兵之战
\n\n当开发者听到“多线程”时,通常会想到操作系统线程。但在Go中,情况完全不同。让我们分析一下关键区别。
\n\n什么是操作系统线程(Thread)?
\n线程是由操作系统内核管理的重量级对象。每个线程都有自己的栈(通常为1-8 MB),创建和切换都需要系统调用。创建10,000个线程就意味着耗尽内存和CPU。Go采用了不同的理念。
\n\nGoroutine——Go的轻量级线程
\nGoroutine是一个并发执行的函数。它通过关键字go启动。主要特点:
- \n
- 栈大小:从2-4 KB开始,并动态增长/收缩。 \n
- 创建:数千个goroutine可在毫秒内创建。 \n
- 调度器:Go运行时自动将goroutine分配到操作系统线程上(通常数量等于CPU核心数)。 \n
- 语法:只需在函数调用前添加
go。 \n
package main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc sayHello(name string) {\n for i := 0; i < 3; i++ {\n fmt.Println("你好,", name)\n time.Sleep(100 * time.Millisecond)\n }\n}\n\nfunc main() {\n // 启动两个goroutine\n go sayHello("Go")\n go sayHello("Goroutine")\n\n // 给goroutine时间执行\n time.Sleep(1 * time.Second)\n fmt.Println("主函数执行完毕")\n}\n\n结果:两个goroutine的消息会交错输出。Go会自动在它们之间切换,互不阻塞。尝试运行这段代码——您会看到“你好,Go”和“你好,Goroutine”交替输出。
\n\nChannel:主要的同步工具
\n\nGoroutine很棒,但它们如何通信?Go默认没有共享内存。相反,它遵循的原则是:“不要通过共享内存来通信;而要通过通信来共享内存”。为此,Go提供了channel(通道)。
\n\nChannel是一个类型化的“管道”,goroutine通过它发送和接收数据。这安全、简单且优雅。
\n\n示例:并行处理数字
\npackage main\n\nimport "fmt"\n\nfunc squareWorker(numbers <-chan int, results chan<- int) {\n for n := range numbers {\n results <- n * n\n }\n close(results)\n}\n\nfunc main() {\n numbers := make(chan int, 5)\n results := make(chan int, 5)\n\n // 启动worker\n go squareWorker(numbers, results)\n\n // 发送数据\n numbers <- 2\n numbers <- 4\n numbers <- 6\n close(numbers)\n\n // 读取结果\n for res := range results {\n fmt.Println("平方值:", res)\n }\n}\n\n在这个示例中,我们使用了带缓冲的channel(第二个参数是缓冲区大小)。Worker goroutine从numbers channel中读取数据,计算平方,然后将结果发送到results channel。主函数发送数字后关闭输入channel,然后从结果channel中读取所有输出。