Go 语言的并发处理是其一大亮点。Go 提供了轻量级的线程称为 goroutine,使得并发编程变得非常简单和高效。Go 的并发模型基于 CSP(通信顺序进程)模型,通过 goroutine 和 channel 来进行协作。
1. Goroutine
Goroutine 是 Go 中的轻量级线程。它比操作系统线程更加高效,每个 goroutine 的内存开销非常小,通常只有几 KB,并且 Go 的调度器会自动管理它们。
1.1 启动 Goroutine
要启动一个 goroutine,只需使用 go 关键字:
package main
import "fmt"
func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
go sayHello() // 启动一个 goroutine
fmt.Println("Hello from Main!")
}
在上述代码中,sayHello() 函数会作为一个 goroutine 被异步执行,而 main() 函数会继续执行。程序的输出顺序不确定,可能是:
Hello from Main!
Hello from Goroutine!
也可能是:
Hello from Goroutine!
Hello from Main!
1.2 并发执行多个 Goroutine
你可以启动多个 goroutine 来并发执行任务:
package main
import "fmt"
func task(id int) {
fmt.Printf("Task %d started\n", id)
}
func main() {
for i := 1; i <= 5; i++ {
go task(i) // 启动 5 个 goroutine
}
fmt.Println("All tasks started")
}
在这个例子中,启动了 5 个 goroutine 执行 task 函数。
2. Channel
Channel 是 Go 中用于在 goroutine 之间进行通信的管道。它是一种强类型的数据结构,可以传递数据,也可以控制 goroutine 之间的同步。通过 channel,多个 goroutine 可以通过发送和接收数据进行协作。
2.1 创建和使用 Channel
你可以使用 make 函数创建一个 channel:
package main
import "fmt"
func main() {
ch := make(chan string) // 创建一个字符串类型的 channel
go func() {
ch <- "Hello from Goroutine!" // 向 channel 发送数据
}()
msg := <-ch // 从 channel 接收数据
fmt.Println(msg)
}
在这个例子中,main 函数创建了一个 channel,并且启动了一个 goroutine 向该 channel 发送数据。主函数从 channel 接收数据并打印。
2.2 关闭 Channel
当你不再需要向 channel 发送数据时,可以关闭 channel。关闭的 channel 无法再发送数据,但是可以继续接收数据,直到 channel 中的所有数据都被接收完。
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "Hello"
close(ch) // 关闭 channel
}()
msg, ok := <-ch
if !ok {
fmt.Println("Channel closed!")
} else {
fmt.Println(msg)
}
}
2.3 在多个 goroutine 之间使用 Channel
如果需要让多个 goroutine 之间通过 channel 进行数据交换,可以使用 select 语句。
package main
import "fmt"
func worker(id int, ch chan string) {
ch <- fmt.Sprintf("Worker %d done", id)
}
func main() {
ch := make(chan string)
for i := 1; i <= 3; i++ {
go worker(i, ch)
}
for i := 1; i <= 3; i++ {
fmt.Println(<-ch) // 从 channel 接收消息
}
}
在这个例子中,启动了 3 个 goroutine,通过 channel 将工作完成的消息传递回主线程。
3. Select 语句
select 语句可以同时监听多个 channel,选择其中一个 channel 的数据进行操作。它类似于 switch 语句,但是用于 channel 操作。
3.1 select 示例
package main
import "fmt"
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch1 <- "From Channel 1"
}()
go func() {
ch2 <- "From Channel 2"
}()
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
在这个例子中,select 会监听 ch1 和 ch2,并打印从其中一个 channel 接收到的消息。
3.2 select 的默认选择
select 语句还有一个 default 分支,当所有 channel 都没有准备好时,default 分支会被执行。
package main
import "fmt"
func main() {
ch := make(chan string)
select {
case msg := <-ch:
fmt.Println(msg)
default:
fmt.Println("No message received!")
}
}
如果 ch 没有数据,default 会被执行。
4. Goroutine 的同步
Go 提供了多种方式来同步多个 goroutine 的执行。最常见的方式有:
- WaitGroup:用于等待一组 goroutine 完成。
- Mutex(互斥锁):用于在多个 goroutine 之间共享资源时保证互斥。
4.1 WaitGroup 示例
sync.WaitGroup 用于等待一组 goroutine 执行完毕。
package main
import (
"fmt"
"sync"
)
func task(id int, wg *sync.WaitGroup) {
defer wg.Done() // 当任务完成时调用 Done
fmt.Printf("Task %d started\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加等待的 goroutine 数量
go task(i, &wg)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("All tasks completed")
}
在这个例子中,sync.WaitGroup 用来确保主程序等待所有 goroutine 执行完毕。
4.2 Mutex 示例
sync.Mutex 用于保护共享资源,确保在同一时间只有一个 goroutine 能访问该资源。
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
var count int
func increment() {
mu.Lock() // 锁定
count++
mu.Unlock() // 解锁
}
func main() {
for i := 0; i < 5; i++ {
go increment()
}
// 等待 goroutine 完成
fmt.Println("Final count:", count)
}
5. 总结
- Goroutine:Go 提供了轻量级的线程,能够在后台并发执行任务,极大提高了并发性能。
- Channel:Go 中 goroutine 之间的通信是通过 channel 完成的,channel 是线程安全的,支持同步和数据传递。
- Select:
select语句可以监听多个 channel,当某个 channel 有数据时,执行对应的代码。 - 同步方式:使用
sync.WaitGroup等工具来等待多个 goroutine 完成;使用sync.Mutex来保证并发访问时的资源安全。
Go 的并发编程模型简单、高效,是进行并发处理的理想选择。通过合理使用 goroutine 和 channel,可以轻松构建并发程序。