06.并发编程
并发编程在 Go
语言中是一项核心功能,使得同时运行多个进程或任务成为可能。
Go
使用协程(goroutines)
和 通道(channels)
来实现并发,这使得编写并发程序变得简单且安全。🦌💻
协程
协程(goroutines) 是由 Go
运行时管理的轻量级线程。
启动一个新的协程非常简单,只需在函数调用前加上 go
关键字。
这个函数将在一个新的协程中异步执行。
// 一个简单的函数
func sayHello() {
fmt.Println("Hello, 世界!")
}
func main() {
// 在新的协程中运行 sayHello 函数
go sayHello()
// 主协程继续执行其他操作
}
在上面的代码中,sayHello
函数将在它自己的协程中执行,而 main
函数会继续执行下一行代码而不会等待 sayHello
。
例子:
package main
import (
"fmt"
"time"
)
// 定义一个接口
type Greeter interface {
Greet() string
}
// 定义一个结构体
type EnglishSpeaker struct{}
// 实现接口方法
func (es EnglishSpeaker) Greet() string {
return "Hello!"
}
// 定义另一个结构体
type ChineseSpeaker struct{}
// 实现接口方法
func (cs ChineseSpeaker) Greet() string {
return "你好!"
}
// 任何满足Greeter接口的类型都可以传递给这个函数
func sayGreeting(g Greeter) {
fmt.Println(g.Greet())
}
// 这个函数会在协程中运行
func greetEverySecond(greeter Greeter) {
for i := 0; i < 5; i++ {
fmt.Println(greeter.Greet())
time.Sleep(1 * time.Second)
}
}
func main() {
var greeter Greeter = EnglishSpeaker{}
// 开始一个新的Go协程
go greetEverySecond(greeter)
// 主线程继续执行其他任务
fmt.Println("这是主线程")
// 等待协程完成
time.Sleep(6 * time.Second)
fmt.Println("主线程结束")
}
输出:
在这个示例中,greetEverySecond
函数会打印问候语并暂停一秒钟,重复五次。
我们使用 go
关键字在新的 Go
协程中执行这个函数,同时主线程继续执行。
例子:
func task1() {
for i := 0; i < 5; i++ {
fmt.Println("Task 1")
time.Sleep(time.Second)
}
}
func task2() {
for i := 0; i < 5; i++ {
fmt.Println("Task 2")
time.Sleep(time.Second)
}
}
func main() {
go task1() // 开启一个新的goroutine来运行task1
go task2() // 开启另一个新的goroutine来运行task2
// 等待足够长的时间以观察到两个任务的输出
time.Sleep(6 * time.Second)
}
输出:
在这个例子中,task1
和 task2
函数几乎同时运行,因为它们分别在自己的 goroutines
中执行。
主函数在等待时不会阻塞这些 goroutines
的执行,所以我们可以看到两个任务的输出几乎是交替出现的。
例子:
package main
import (
"fmt"
"sync"
"time"
)
/*
goroutine 开启时进行 wg.Add(1) 加 1
goroutine 结束时进行 wg.Done() 减 1
wg.Wait() 会判断当前的 goroutine 是否为 0,为 0 则退出
*/
// 定义一个计数器
var wg sync.WaitGroup
func main() {
// 开启一个协程计数器+1
wg.Add(1)
go test()
// 计数器为0时则退出
wg.Wait()
fmt.Println("主函数运行结束!")
}
func test() {
for i := 0; i < 10; i++ {
fmt.Println(i)
time.Sleep(100 * time.Microsecond)
}
// 协程执行完毕,计数器-1
wg.Done()
}
输出:
通道
通道(channels) 是用来在协程之间安全地传递数据的管道。
你可以发送数据到一个通道,并在另一个通道接收数据。
// 创建一个传递 int 类型数据的通道
ch := make(chan int)
// 在新的协程中发送数据到通道
go func() {
ch <- 42 // 把 42 发送到通道
}()
// 在主协程中接收通道的数据
val := <-ch
fmt.Println("接收到的值:", val) // 输出接收到的值: 42
在这个例子中,我们创建了一个通道 ch
,在一个新的协程中向它发送了一个值 42
,然后在主协程中接收并打印这个值。
例子:
// 创建一个通道用来传递数据
ch := make(chan int)
// 发送者goroutine
go func() {
for i := 0; i < 5; i++ {
// 发送数据到通道
ch <- i
}
close(ch) // 发送完成后,关闭通道
}()
// 接收者goroutine
go func() {
for value := range ch {
// 从通道接收数据
fmt.Println("Received:", value)
}
}()
在这个例子中,我们创建了一个通道 ch
,一个 goroutine
用于发送数据,另一个 goroutine
用于接收数据。
当发送者完成发送后,它会关闭通道来通知接收者没有更多的数据。
并发与通道示例
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个无缓冲的通道
ch := make(chan int)
// 生产者goroutine,发送数据到通道
go func() {
for i := 0; i < 5; i++ {
fmt.Printf("Sent: %d\n", i)
ch <- i // 将i发送到通道
time.Sleep(time.Second) // 模拟耗时的发送操作
}
close(ch) // 发送完成后关闭通道
}()
// 消费者goroutine,从通道接收数据
go func() {
// 使用for range循环从通道接收数据,直到通道被关闭
for value := range ch {
fmt.Printf("Received: %d\n", value)
}
}()
// 主函数等待足够的时间,确保所有数据都被发送和接收
// 这里我们只是简单地使用Sleep,实际项目中你可能会使用sync包中的WaitGroup
time.Sleep(7 * time.Second)
fmt.Println("Finished processing")
}
输出:
在这个示例中,我们定义了一个名为 main
的函数,它是程序的入口点。
它首先创建了一个无缓冲的通道 ch
。然后,启动了两个 goroutine
。
- 生产者
goroutine
在一个for
循环中发送0
到4
的数字,每次发送后休眠一秒钟来模拟耗时的操作。发送完成后,它使用close
函数关闭了通道,以通知消费者没有更多的数据将会发送。 - 消费者
goroutine
使用for range
循环来接收通道ch
中的数据。由于通道被关闭,循环会在接收完所有数据后退出。
main
函数在最后使用 time.Sleep
来等待一段时间,这确保了所有的数据都能够被发送和接收。
在实际的应用中,我们通常会使用 sync.WaitGroup
或其他同步机制来等待所有的 goroutine
完成,而不是使用 time.Sleep
。
并发与通道进阶
并发在 Go
中通过 goroutines
实现,它们是由 Go
运行时环境调度的轻量级线程。
goroutines
可以使用 通道(channels)
来通信。
例子:
package main
import (
"fmt"
"sync"
"time"
)
// 一个工作函数,模拟耗时的任务
func worker(id int, wg *sync.WaitGroup, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d started job %d\n", id, job)
time.Sleep(time.Second) // 模拟工作耗时
fmt.Printf("Worker %d finished job %d\n", id, job)
results <- job * 2 // 将结果发送到结果通道
wg.Done() // 通知WaitGroup任务已完成
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// 启动三个worker,初始时都在等待
for w := 1; w <= 3; w++ {
wg.Add(1) // 增加WaitGroup计数器
go worker(w, &wg, jobs, results)
}
// 发送工作任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // 关闭通道表示不再发送新工作
// 等待所有goroutines完成
wg.Wait()
close(results) // 关闭结果通道
// 收集所有的结果
for result := range results {
fmt.Printf("Job result: %d\n", result)
}
}
输出:
这个例子中我们创建了一个工作队列和一个结果队列,然后启动了三个 worker
goroutines
去处理作业。
我们使用了 sync.WaitGroup
来等待所有作业完成。
每个 worker
在完成作业时,都会向 results
通道发送一个结果,并通过 wg.Done()
通知 WaitGroup
一个作业已完成。
main
函数等待所有作业完成后,关闭结果通道,并收集和打印出所有结果。
选择语句
选择语句(select) 用于等待多个通道操作。
它会阻塞,直到一个或多个通道准备好通信。
select {
case val := <-ch:
fmt.Println("从通道接收到:", val)
case <-time.After(50 * time.Millisecond):
fmt.Println("超时了!")
}
这个 select
语句等待通道 ch
接收数据或超时发生(使用 time.After
)。
并发编程可以提高程序的性能,特别是在处理多个独立任务或高延迟操作时。
但它也引入了竞争条件和同步问题。
Go
的通道和协程提供了一种相对简单的方式来处理这些挑战。