Go中的并发与多线程:从goroutine到channel
Go语言(Golang)由Google开发,充分考虑了现代对性能和可扩展性的需求。其主要"特色"是内置的并发支持,该支持在语言层面实现,而非通过外部库。与Python中多线程受GIL(全局解释器锁)限制不同,Go提供了轻量级的goroutine,可以高效地同时执行数千个任务。在本文中,我们将通过实际代码示例,深入探讨Go中并发与多线程的关键概念,包括goroutine、channel、mutex和WaitGroup。
1. Goroutine:Go的轻量级线程
Goroutine是一个与同一地址空间中的其他函数并发执行的函数。它运行在系统线程之上,但使用Go自己的调度器,这使得它比普通的OS线程轻量得多。创建goroutine使用关键字go。
package main
import ( "fmt" "time")
func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) }}
func main() { go say("Hello") // 启动goroutine say("World") // 在主线程中普通调用}在这个例子中,say("Hello")在单独的goroutine中执行,而say("World")在主线程中执行。由于两个函数并发运行,输出会交错。重要提示:如果主函数main结束,所有goroutine都会被强制停止。
2. Channel:goroutine之间的安全数据传输
Channel是类型化的"管道",goroutine通过它相互通信。它们无需显式使用mutex即可实现同步。Channel分为带缓冲的(有容量)和不带缓冲的(同步)。
无缓冲Channel
package main
import "fmt"
func sum(s []int, c chan int) { total := 0 for _, v := range s { total += v } c <- total // 将结果发送到channel}
func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int)
go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从channel接收
fmt.Println(x, y, x+y)}这里两个goroutine计算数组两半的和,并将结果发送到channel。主goroutine会阻塞,直到收到两个值。无缓冲channel就像一个同步点:发送会阻塞,直到接收方准备好,反之亦然。
带缓冲Channel
package main
import "fmt"
func main() { ch := make(chan int, 2) // 缓冲区大小为2 ch <- 1 ch <- 2 // ch <- 3 // 这会导致死锁,因为缓冲区已满 fmt.Println(<-ch) fmt.Println(<-ch)}带缓冲channel允许在缓冲区未满时无阻塞地发送数据。这在发送速度高于处理速度时非常有用。
3. 同步:WaitGroup和Mutex
虽然channel覆盖了大多数场景,但有时需要对同步进行更精细的控制。为此,sync包提供了WaitGroup(等待一组goroutine完成)和Mutex(用于保护共享数据的互斥锁)。
WaitGroup:等待所有goroutine完成
package main
import ( "fmt" "sync")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // 完成时减少计数器 fmt.Printf("Worker %d starting", id) // 模拟工作 fmt.Printf("Worker %d done", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // 增加计数器