Go语言中的并发与多线程:从入门到精通
Go(Golang)在设计之初就考虑到了现代计算负载。在多核处理器和云服务时代,高效处理并发已成为后端开发人员的必备技能。本文将深入探讨Go如何解决并行代码执行问题,goroutine与操作系统线程的区别,以及如何正确使用channel和互斥锁。
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("来自goroutine的问候") say("来自main的问候") // 给goroutine一些时间完成 time.Sleep(500 * time.Millisecond)}请注意:程序在main()函数执行完毕后立即结束,不会等待已启动的goroutine完成。为了同步,我们使用了time.Sleep(在实际项目中应使用sync.WaitGroup或channel)。
2. Channel:安全的数据传输
Channel是类型化的“管道”,goroutine通过它交换数据。它们解决两个关键问题:执行同步和值传递。Channel分为有缓冲和无缓冲两种。
无缓冲Channel
发送操作会阻塞goroutine,直到另一个goroutine执行读取操作。这实现了“即时”同步。
package main
import "fmt"
func main() { ch := make(chan string)
go func() { ch <- "来自goroutine的问候!" }()
msg := <-ch fmt.Println(msg)}有缓冲Channel
带缓冲的channel允许在无需立即读取的情况下发送多个值,直到缓冲区填满。
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和close
关闭channel会通知接收方不再有数据。当channel关闭时,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("收到:", val) }}3. 同步与互斥锁
尽管channel功能强大,但有时需要直接访问共享数据。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("最终计数器:", counter)}这里sync.WaitGroup等待所有goroutine完成,而互斥锁防止了数据竞争。没有互斥锁,结果将是不可预测的。