原文链接
https://dave.cheney.net/paste/concurrency-made-easy.pdf
原文对 go 的并发中常常出现的bug 提出了一些建议,本文是对原文的建议做一些总结和自己的见解
不必要的goroutine的使用
如果一个goroutine在还没有获得另一个goroutine的返回结果时,无法取得进展,那么不如放弃goroutine的使用,只用这个goroutine完成工作
package main
import (
"fmt"
"log"
"net/http"
"runtime"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, GopherCon SG")
})
go func() {
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}()
for {} // 第一种阻塞的方式
//for {
// runtime.Gosched() //第二种
//}
//select {} //第三种
}
以上例子中的三种方法来阻塞goroutine 都是治标不治本 。
倒不如直接把 ListenAndServe 放在 同一个goroutine中运行
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, GopherCon SG")
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
按照获取锁和channel值的相反顺序释放它们
func restore(repos []string) error {
errChan := make(chan error, 1)
sem := make(chan int, 4) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos {
sem <- 1
go func() {
defer func() {
wg.Done()
<-sem
}()
if err := fetch(repo); err != nil {
errChan <- err
}
}()
}
wg.Wait()
close(sem)
close(errChan)
return <-errChan
}
在这个例子中
defer func() {
wg.Done()
<-sem
wg.Wait()
close(sem)
在这两处地方, close(sem) happens after wg.Wait() ,因此 close(sem) 也 happens after wg.Done()
当wg.Done() 后,waitgroup不再等待 ,所以无法知晓 <-sem happens-before close(sem) 还是 close(sem) happens-before <-sem ,所以可能会出现bug
这种情况的一种解决办法就是 把 <-sem 写在goroutine 里面 <-sem happens-before wg.Done()
func restore(repos []string) error {
errChan := make(chan error, 1)
sem := make(chan int, 4) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos {
sem <- 1
go func() {
defer wg.Done()
if err := fetch(repo); err != nil {
errChan <- err
}
<-sem //这里
}()
}
wg.Wait()
close(sem)
close(errChan)
return <-errChan
}
channel不像其他的资源,没有必要为了释放它们而close
close()的作用是关闭通道,让cahnnel不再接收值,而不是释放channel 资源
Acquire semaphores when you’re ready to use them.
尽管goroutine的创建和调度都很便宜,但是它们所使用的资源,比如文件、socket、带宽等等,通常都比较稀缺。使用通道作为信号量来限制正在进行的工作的模式非常常见。
但是,为了确保不会过度地阻塞goroutine的代码加载工作,请在准备好使用它们时获取信号量,而不是期望使用它们时获取信号量。
避免出现goroutine 的数据竞争
func restore(repos []string) error {
errChan := make(chan error, 1)
sem := make(chan int, 4) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos { //这里
sem <- 1
go func() {
defer wg.Done()
if err := fetch(repo); err != nil { //这里
errChan <- err
}
<-sem
}()
}
wg.Wait()
close(sem)
close(errChan)
return <-errChan
还是这个例子中,每个goroutine都需要for循环中的repo,所以会出现所有 goroutine 都会试图去读最后一个repo的值
解决办法是将 repo 作为参数传进goroutine
func restore(repos []string) error {
errChan := make(chan error, 1)
sem := make(chan int, 4) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for i := range repos {
go func(repo string) { //这里
defer wg.Done()
sem <- 1
if err := fetch(repo); err != nil {
errChan <- err
}
<-sem
}(repos[i]) //这里
}
wg.Wait()
close(errChan)
return <-errChan
}
避免匿名函数 和 goroutine 混合使用
func restore(repos []string) error {
errChan := make(chan error, 1)
sem := make(chan int, 4) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos {
go worker(repo, sem, &wg, errChan)
}
wg.Wait()
close(errChan)
return <-errChan
}
func worker(repo string, sem chan int, wg *sync.WorkGroup, errChan chan err) {
defer wg.Done()
sem <- 1
if err := fetch(repo); err != nil {
errChan <- err
}
<-sem
}
这样写就会避免出现上文 repo 的数据竞争
在你开始一个goroutine之前,要知道它何时以何种方式停止
func restore(repos []string) error {
errChan := make(chan error, 1) //这里
sem := make(chan int, 4) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos {
go worker(repo, sem, &wg, errChan)
}
wg.Wait()
close(errChan)
return <-errChan
}
func worker(repo string, sem chan int, wg *sync.WorkGroup, errChan chan err) {
defer wg.Done()
sem <- 1
if err := fetch(repo); err != nil {
errChan <- err //这里
}
<-sem
}
还是这个例子,注意errChan 是一个缓冲为1的channel,如果所有的go worker 都出现err 那么errChan就会阻塞
一种解决办法是 将errChan的大小设置为len(repos)
func restore(repos []string) error {
errChan := make(chan error, len(repos)) //这里
sem := make(chan int, 4) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos {
go worker(repo, sem, &wg, errChan)
}
wg.Wait()
close(errChan)
return <-errChan
}
func worker(repo string, sem chan int, wg *sync.WorkGroup, errChan chan err) {
defer wg.Done()
sem <- 1
if err := fetch(repo); err != nil {
errChan <- err
}
<-sem
}
我们还可以结合select的使用来解决 ,不必为所有可能的错误开辟空间 ,而是可以使用非阻塞发送将错误放入errChan(如果不存在的话),否则将丢弃该值。
func restore(repos []string) error {
errChan := make(chan error, 1)
sem := make(chan int, 4) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos {
go worker(repo, sem, &wg, errChan)
}
wg.Wait()
close(errChan)
return <-errChan
}
func worker(repo string, sem chan int, wg *sync.WorkGroup, errChan chan err) {
defer wg.Done()
sem <- 1
if err := fetch(repo); err != nil {
select {
case errChan <- err:
// we're the first worker to fail
default:
// some other failure has already happened
}
}
<-sem
}