Go语言中的并发与多线程:从入门到精通
Go(Golang)从设计之初就充分考虑了现代计算工作负载。在多核处理器和云服务时代,有效处理并发的技能已成为后端开发人员的必备能力。在本文中,我们将深入探讨Go如何解决并行代码执行问题,goroutine与操作系统线程有何不同,以及如何正确使用通道和互斥锁。
1. Goroutine:轻量级线程
Goroutine是与其他goroutine并发执行的函数或方法。与操作系统线程不同,goroutine的开销极小:其栈空间初始仅为2-4KB,并且可以动态增长。启动goroutine非常简单——只需在函数调用前添加关键字go即可。
package main
import ( "fmt" "time")
func say(text string) { for i := 0; i < 3; i++ { fmt.Println(text) time.Sleep(100 * time.Millisecond) }}
func main() { go say("Hello from goroutine") say("Hello from main") // 给goroutine时间完成执行 time.Sleep(500 * time.Millisecond)}注意:程序在main()函数执行完毕后立即终止,不会等待已启动的goroutine完成。为了同步,我们使用了time.Sleep(在实际项目中,应使用sync.WaitGroup或通道)。
2. 通道:安全的数据传输
通道是类型化的"管道",goroutine通过它交换数据。通道解决两个关键任务:执行同步和值传递。通道可以是有缓冲的或无缓冲的。
无缓冲通道
发送操作会阻塞当前goroutine,直到另一个goroutine执行读取操作。这提供了"即时"同步。
package main
import "fmt"
func main() { ch := make(chan string)
go func() { ch <- "Hello from goroutine!" }()
msg := <-ch fmt.Println(msg)}有缓冲通道
带有缓冲区的通道允许在不立即读取的情况下发送多个值,直到缓冲区被填满。
package main
import "fmt"
func main() { ch := make(chan int, 2) ch <- 10 ch <- 20 // ch <- 30 // 死锁!缓冲区已满
fmt.Println(<-ch) // 10 fmt.Println(<-ch) // 20}Range遍历与关闭
关闭通道向接收方表明不再有数据发送。for range循环在通道关闭时自动终止。
package main
import "fmt"
func producer(ch chan int) { for i := 1; i <= 5; i++ { ch <- i } close(ch)}
func main() { ch := make(chan int) go producer(ch)
for val := range ch { fmt.Println("Received:", val) }}3. 同步与互斥锁
尽管通道功能强大,但有时仍需要直接访问共享数据。sync包提供了互斥锁(sync.Mutex)和读写互斥锁(sync.RWMutex)来保护临界区。
package main
import ( "fmt" "sync")
var counter = 0var mu sync.Mutex
func increment(wg *sync.WaitGroup) { defer wg.Done() mu.Lock() counter++ mu.Unlock()}
func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go increment(&wg) } wg.Wait() fmt.Println("Final counter:", counter)}这里,sync.WaitGroup等待所有goroutine执行完毕,互斥锁则防止数据竞争。如果没有互斥锁,结果将是不可预测的。