Golang 协程与通道
前言
在 Go 语言中,
协程被称为 goroutines。goroutine 是 Go 的并发执行单元,它比传统的线程更轻量级,允许你以非常低的开销启动成千上万个并发任务
通道(channel)是一种特殊的类型,用于在不同的 goroutines 之间进行通信和同步。通道可以被想象成一个传递数据的管道,它可以帮助确保并发程序的数据同步,避免竞态条件
协程
goroutine 在使用上非常简单,只需要在函数调用前加上关键字 go。当 go 关键字用于调用一个函数时,该函数会在一个新的 goroutine 中异步执行
package main
import (
"fmt"
"time"
)
// 这是一个将在goroutine中运行的函数
func sayHello() {
fmt.Println("Hello, World!")
}
func main() {
// 启动一个goroutine
go sayHello()
// 主goroutine需要等待一段时间,否则程序可能在 sayHello goroutine运行之前就退出了
time.Sleep(1 * time.Second)
fmt.Println("Main function finished")
}
sayHello 函数在一个新的 goroutine 中被调用。主 main 函数在调用 sayHello 后会继续执行,几乎立即执行 time.Sleep 来等待一秒钟,确保有足够的时间让 sayHello goroutine 执行。如果没有 time.Sleep,程序可能会在 sayHello goroutine 还没来得及运行时结束,因为主函数(也是一个 goroutine)结束时会关闭所有的其他 goroutines
注意:在实际程序中,我们通常不会使用 time.Sleep 来等待一个 goroutine 完成,因为这种方法并不可靠。相反,我们通常会使用通道(channels)或其他同步机制,如 WaitGroups,来协调不同 goroutines 之间的执行
通道
通道可以是带缓冲的或不带缓冲的
- 不带缓冲的通道在发送和接收数据时会阻塞,直到另一端准备好进行接收或发送操作
- 带缓冲的通道则允许在阻塞之前存储一定数量的值
创建通道:
// 创建一个不带缓冲的通道
ch := make(chan int)
// 创建一个带缓冲的通道,缓冲大小为2
bufferedCh := make(chan int, 2)
向通道发送数据和从通道接收数据:
// 发送数据到通道
ch <- value
// 从通道接收数据,并将值存储在变量中
value := <-ch
// 或者,仅从通道接收数据,不存储值
<-ch
举个使用通道在两个 goroutines 之间发送和接收数据的例子:
package main
import (
"fmt"
"time"
)
func sendMessage(ch chan string) {
// 向通道发送消息
ch <- "Hello, World!"
}
func main() {
// 创建一个不带缓冲的通道
ch := make(chan string)
// 启动一个goroutine来发送消息
go sendMessage(ch)
// 从通道接收消息
message := <-ch
fmt.Println(message)
}
在这个例子中,sendMessage goroutine 向 ch 通道发送了一个字符串。主函数中的 <-ch 语句会阻塞,直到有数据可以从通道中接收。一旦 sendMessage goroutine 发送了数据,主函数就会接收到消息并打印出来
通道是实现 Go 并发模型的核心,利用通道可以构建复杂的并发程序,同时保持代码的安全性和清晰性
常用的两种同步机制:通道与WaitGroups
通道介绍过了,再来一个例子:
func worker(done chan bool) {
fmt.Print("Working...")
time.Sleep(time.Second)
fmt.Println("done")
// 发送信号完成工作
done <- true
}
func main() {
done := make(chan bool, 1)
go worker(done)
// 等待工作完成
<-done
}
在这个例子中,main 函数启动了一个 worker goroutine,并传递了一个通道 done。worker 完成工作后,向 done 通道发送了一个信号(一个 true 值)。main 函数通过 <-done 表达式等待这个信号,这确保了 main 函数会等待 worker 完成工作才继续执行
WaitGroup
sync.WaitGroup 是一个等待一组 goroutines 完成的同步机制。主要的方法包括 Add(增加等待的 goroutines 数),Done(goroutine 完成时调用),和 Wait(等待所有 goroutine 完成)
示例:
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 完成时通知 WaitGroup
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 增加等待的 goroutines 数
go worker(i, &wg)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("All workers completed")
}
main 函数创建了一个 WaitGroup 并启动了五个 worker goroutines。每个 worker 在开始时通过 defer wg.Done() 确保在函数返回时通知 WaitGroup 完成。main 函数使用 wg.Wait() 来阻塞,直到所有的 worker goroutines 都调用了 Done 方法
死锁或 panic
在使用 sync.WaitGroup 时,如果不正确地管理协程的启动与结束,就可能遇到死锁或 panic 的情况
- 死锁 (Deadlock): 这通常发生在 Wait 被调用,但是 WaitGroup 的计数器没有被正确减至0的情况。在这种场合,程序将永远等待,因为没有更多的操作能够减少计数器的值
例如:
var wg sync.WaitGroup
wg.Add(1)
// 没有启动任何协程来调用 wg.Done()
wg.Wait() // 这里会发生死锁,因为没有任何操作能使计数器变为0
- Panic: 如果调用 Done 方法的次数比通过 Add 方法设置的值还要多,程序将会 panic,错误信息大致为 “negative WaitGroup counter” 或类似的信息。这表明你试图减少一个已经为0的计数器
例如:
var wg sync.WaitGroup
// 没有调用 wg.Add() 来增加计数器
wg.Done() // 这里会导致 panic,因为计数器变成了负数
因而,为了避免这些问题,确保:
- 使用 wg.Add(n) 时,确保后续有足够的 wg.Done() 调用与之对应
- 不要在所有协程启动之前调用 wg.Wait()
- 在使用 wg.Add(1) 后应立即启动协程
- 在协程内部,只有当实际工作完成后,才调用 wg.Done()
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!