09
2020
04

Golang 中常用的并发模型?

Golang 中常用的并发模型有三种:


通过channel通知实现并发控制

无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才可以完成发送和接收操作。


从上面无缓冲的通道定义来看,发送 goroutine 和接收 gouroutine 必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。


func main() {

    ch := make(chan struct{})

    go func() {

        fmt.Println("start working")

        time.Sleep(time.Second * 1)

        ch <- struct{}{}

    }()


    <-ch


    fmt.Println("finished")

}

当主 goroutine 运行到 <-ch 接受 channel 的值的时候,如果该 channel 中没有数据,就会一直阻塞等待,直到有值。 这样就可以简单实现并发控制


通过sync包中的WaitGroup实现并发控制

Goroutine是异步执行的,有的时候为了防止在结束mian函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它会等待它收集的所有 goroutine 任务全部完成。在WaitGroup里主要有三个方法:


Add, 可以添加或减少 goroutine的数量.

Done, 相当于Add(-1).

Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0.

在主 goroutine 中 Add(delta int) 索要等待goroutine 的数量。 在每一个 goroutine 完成后 Done() 表示这一个goroutine 已经完成,当所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。


func main(){

    var wg sync.WaitGroup

    var urls = []string{

        "http://www.golang.org/",

        "http://www.google.com/",

    }

    for _, url := range urls {

        wg.Add(1)

        go func(url string) {

            defer wg.Done()

            http.Get(url)

        }(url)

    }

    wg.Wait()

}

在Golang官网中对于WaitGroup介绍是A WaitGroup must not be copied after first use,在 WaitGroup 第一次使用后,不能被拷贝


应用示例:


func main(){

 wg := sync.WaitGroup{}

    for i := 0; i < 5; i++ {

        wg.Add(1)

        go func(wg sync.WaitGroup, i int) {

            fmt.Printf("i:%d", i)

            wg.Done()

        }(wg, i)

    }

    wg.Wait()

    fmt.Println("exit")

}

运行:


i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!


goroutine 1 [semacquire]:

sync.runtime_Semacquire(0xc000094018)

        /home/keke/soft/go/src/runtime/sema.go:56 +0x39

sync.(*WaitGroup).Wait(0xc000094010)

        /home/keke/soft/go/src/sync/waitgroup.go:130 +0x64

main.main()

        /home/keke/go/Test/wait.go:17 +0xab

exit status 2

它提示所有的 goroutine 都已经睡眠了,出现了死锁。这是因为 wg 给拷贝传递到了 goroutine 中,导致只有 Add 操作,其实 Done操作是在 wg 的副本执行的。


因此 Wait 就死锁了。


这个第一个修改方式:将匿名函数中 wg 的传入类型改为 *sync.WaitGrou,这样就能引用到正确的WaitGroup了。 这个第二个修改方式:将匿名函数中的 wg 的传入参数去掉,因为Go支持闭包类型,在匿名函数中可以直接使用外面的 wg 变量


在Go 1.7 以后引进的强大的Context上下文,实现并发控制

通常,在一些简单场景下使用 channel 和 WaitGroup 已经足够了,但是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 显得有些力不从心了。 比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine,比如数据库和RPC服务。 所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的 Context,称之为上下文非常贴切,它就是goroutine 的上下文。 它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个 Context 里,再将它传给要执行的 goroutine 。


context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理。


context 包的核心是 struct Context,接口声明如下:


// A Context carries a deadline, cancelation signal, and request-scoped values

// across API boundaries. Its methods are safe for simultaneous use by multiple

// goroutines.

type Context interface {

    // Done returns a channel that is closed when this `Context` is canceled

    // or times out.

    Done() <-chan struct{}


    // Err indicates why this Context was canceled, after the Done channel

    // is closed.

    Err() error


    // Deadline returns the time when this Context will be canceled, if any.

    Deadline() (deadline time.Time, ok bool)


    // Value returns the value associated with key or nil if none.

    Value(key interface{}) interface{}

}

Done() 返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号


Err() 在Done() 之后,返回context 取消的原因。


Deadline() 设置该context cancel的时间点


Value() 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。


Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。


一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。 其中的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。 典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。

« 上一篇 下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。