对 concurrency-made-easy 文章的总结

原文链接

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
 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值