golang并发基础
0. CSP--Communicating Sequential Process
Don't communicate by sharing memory; share memory by communicating.
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。Go语言CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
1. go协程(go routine)
go原生支持并发:goroutine和channel。
go协程是与其他函数或方法一起并发运行的函数和方法。go协程可以看作是轻量级线程。
func runtime.GOMAXPROCS(n) int -- sets the maximum number of CPUs that can be executing simultaneously and returns the previous setting.
调用函数或者方法时,在前面加上关键字go,可以让一个新的GO协程并发地运行。
- l 启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。
- l 如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。
常见的问题:go协程引用外部循环变量(参考:协程引用循环变量的问题)
//mu := &sync.Mutex for i := 0; i < 10; i++ { go func() { // mu.Lock() // mu.Unlock() fmt.Println(i) } () }
理想状态时输出1到10,无论是否有mutex,都不可能输出正确结果。大部分输出的数字是10,原因:
主协程的循环很快就跑完了,而各个协程才开始跑,此时i
的值已经是10
了,所以各协程都输出了10
。(输出7或其他数字
的协程,在开始输出的时候主协程的i
值刚好是7
,这个结果每次运行输出都不一样)
如果通过go vet main.go会报如下警告: main.go:24:16: loop variable i captured by func literal
for i := 0; i < 10; i++ { go func(i0 int) { fmt.Println(i0) } (i) // }
变量i
已经有了一个副本,协程中针对副本处理。
使用goroutine原则:(管理goroutine生命周期)
1)goroutine什么时候终止;2)怎样终止goroutine。
2. 无缓冲信道channel
信道可以想象成Go协程之间通信的管道。
chan T 表示 T 类型的信道。信道与类型相关,只能运输该类型的数据。
信道是一种引用类型,信道的零值为 nil。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道或初始化信道。
var ch chan int // 声明信道
fmt.Println(ch) // <nil>
ch = make(chan int) //初始化
fmt.Println(ch) // 0xc000020180
data := <- a // 读取信道 a a <- data // 写入信道 a
无缓冲信道发送与接收默认是阻塞的,即无缓冲信道是同步的。
信道会产生死锁。当Go协程给一个信道发送数据,而没有信道接收数据时,程序触犯panic,形成死锁。
信道数据都是单传递的,当一个接收信道收到数据后,其他信道接收者就不能接收到相同数据,即信道数据只能由任意一个且仅有一个信道接收者获取到。
对一个nil信道发送或者接收数据都会造成永远阻塞。例如没有make初始化或定义的信道。
单向信道
sendch := make(chan<- int) // 定义单向信道,定义只写数据的信道,<-指向chan
只写通道:chan<- T
只读通道:<-chan T
有方向的channel不能被关闭。
可以把一个双向信道转换成唯送信道或者唯收信道(send only or receive only),但反过来不可以。
package main import ( "fmt" "time" "os" ) func main(){ data := make(chan int) go func(out chan<- int){ time.Sleep(2* time.Second) out <- 1 }(data) <- data fmt.Println("Receive data, first") go func(out <-chan int){ time.Sleep(2 * time.Second) <-out fmt.Println("Receive data, Second") os.Exit(0) }(data) data <- 2 for { time.Sleep(1 * time.Second) } }
关闭信道
close(ch)
数据发送方可以关闭信道,通知接收方这个信道不再有数据发送过来。
当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭。
v, ok := <- ch
如果可以从信道接收数据,ok等于true;如果信道关闭,ok等于false。
所有的channel接收者都会在channel关闭时,立刻从阻塞等待中返回且上述ok值为false。这个广播机制常被利用,向多个订阅者同时发送信号。如:退出信号。
从一个关闭的信道中读取到的值是该信道类型的零值。向关闭的channel发送数据,会导致panic。
panic:关闭未初始化的channel;重复关闭channel
range遍历信道
for range 循环用于在一个信道关闭之前,从信道接收数据。一旦关闭了信道,循环自动结束(不用像 v,ok :=<- ch一样增加ok判断)。
ch := make(chan int) go producer(ch) for v := range ch { fmt.Println("Received ",v) }
信道关闭后可以读取到信道数据, 一个示例:
func main(){ sch := make(chan int) go func(){ sch <- 100 close(sch) fmt.Println(time.Now().UnixNano(), " close channel sch") }() fmt.Println(time.Now().UnixNano(), " start") time.Sleep(3*time.Second) //无缓冲通道是同步的,阻塞的,只有读取通道后才能执行下一步,发送接收同时(未验证),先接收100 // close channel sch(晚于100)和 接收到的数据(close后)差不多同时(同一时刻)。 所有goroutines都要等待3s,因为信道阻塞 for j := 0; j < 10; j++ { go func(){ // 因为信道只发送了一个数据,只能有一个信道接收到数据,其余信道在close后返回信道零值 vv, ok := <-sch fmt.Println(time.Now().UnixNano(), " ", vv, ok) }() } time.Sleep(3*time.Second) //主进程延迟退出 }
执行结果(goroutines执行顺序不确定):
1596192228730834200 start 1596192231731488800 100 true 1596192231731488800 0 false 1596192231731488800 close channel sch 1596192231731488800 0 false 1596192231731488800 0 false 1596192231731488800 0 false 1596192231731488800 0 false 1596192231731488800 0 false 1596192231731488800 0 false 1596192231731488800 0 false 1596192231731488800 0 false
要让一个信道有缓冲,make的 capacity
应该大于 0。无缓冲信道的容量默认为 0。
3. 缓冲信道
信道的接收和发送都是阻塞的,当多个信道发送者向一个信道发送数据时,接收信道在接收一个信道发送的数据后处理其他任务(不再处理该信号数据),会导致其他发送信道协程阻塞,造成协程泄露。
runtime.NumGoroutine() // 获取当前协程数
buffered Channel可以接收多个信道数据,从而排除阻塞。(仅需任意任务完成即可)
buffered Channel只有缓冲已满的情况才会阻塞发送数据,同样只有缓冲为空时才阻塞接收数据,缓冲信道是异步的。
ch := make(chan type, capacity) // capacity大于0
缓冲信道的容量是指信道可以存储的值的数量。缓冲信道的长度是指信道中当前排队的元素个数。
func main() { ch := make(chan int, 5) done := make(chan bool) go func() { for v := range ch { fmt.Println(time.Now().Unix(), " ", v) if v == 3 { fmt.Println(time.Now().Unix(), " close channel") close(ch) break } } done <- true }() go func() { for i := 0; i < 5; i++ { ch <- i } }() <-done //信道关闭后仍能读取到信道数据,正确数据接收完后,会接收信道零值数据 for { v, ok := <-ch fmt.Println(time.Now().Unix(), " ", v, ok) time.Sleep(time.Second) } }
1596193952 0 1596193952 1 1596193952 2 1596193952 3 1596193952 close channel 1596193952 4 true 1596193953 0 false 1596193954 0 false
比如,要自己手写实现个简单的数据库连接池。在Java里,我们需要一个数组来存放数据库连接,连接池的所有操作方法都要对其加上锁,以确保两个线程没有共用一个连接,连接池里没连接了或连接满了怎么办等。然而在Go里,我们只需要一个具有缓冲区的channel就行了:
pool := make(chan mysql.Conn, size) conn := <-p.pool //从连接池取一个连接 p.pool <- conn //在把连接放回连接池
三句简单的代码组成了实现一个完美支持多线程的数据库连接池类库的核心基石,剩下的就是对该类库功能上的完美封装与优化了。
4.工作池
WaitGroup
用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。
定义:var wg sync.WaitGroup
调用:wg.Add(1)…wg.Done()…wg.Wait()
注:wg.Add()参数不能是负数,否则panic。
package main import ( "fmt" "sync" "time" ) func process(i int, wg *sync.WaitGroup) { fmt.Println("started Goroutine ", i) time.Sleep(2 * time.Second) fmt.Printf("Goroutine %d ended\n", i) wg.Done() } func main() { no := 3 var wg sync.WaitGroup for i := 0; i < no; i++ { wg.Add(1) go process(i, &wg) } wg.Wait() fmt.Println("All go routines finished executing") } output: started Goroutine 2 started Goroutine 0 started Goroutine 1 Goroutine 1 ended Goroutine 2 ended Goroutine 0 ended All go routines finished executing
协程中传参wg地址非常重要,wg.Done()执行完毕后主协程才知道。若是值拷贝,main函数不知道。
package main import ( "fmt" "math/rand" "sync" "time" ) type Job struct { id int digital int } type Result struct { Job result int no int } var numTasks = 10 //任务数 var numMasterCh = 5 //分发通道数 var numWorkers = 3 //worker数 var numWorkerCh = 3 //worker通道数 var chMaster = make(chan Job, numMasterCh) var chWorker = make(chan Result, numWorkerCh) func sumOfDigitals(num int) int { sum := 0 for num != 0 { sum = sum + num%10 num = num / 10 } time.Sleep(time.Second) return sum } // master distribute work func doMasterDistribution(count int) { for i := 0; i < count; i++ { job := Job{id: i, digital: rand.Intn(999)} chMaster <- job } close(chMaster) } // master collect results func doMasterResult(Done chan bool) { for result := range chWorker { fmt.Println(time.Now().Format("2006-01-02 15:04:05"), " input: ", result.Job, " Result: ", result.result, " Goroutine: ", result.no) } Done <- true } // worker deal with work //func doWorker(chMaster chan Job, wg *sync.WaitGroup, count int) chan Result { //func doWorker(wg *sync.WaitGroup, count int) { func doWorker(count int) { var wg sync.WaitGroup for i := 0; i < count; i++ { wg.Add(1) go func(wg *sync.WaitGroup, i int) { for job := range chMaster { result := sumOfDigitals(job.digital) chWorker <- Result{Job: job, result: result, no: i} } wg.Done() }(&wg, i) } // return chWorker // 信道的接收和发送默认都是阻塞的,对于发送而言,如果没有接收处理完,阻塞,不会执行下一句 wg.Wait() close(chWorker) } func main() { startTime := time.Now() // var wg sync.WaitGroup // buffered channel的输入元素个数大于容量时不会返回,有阻塞危险。 所以用go立即返回 go doMasterDistribution(numTasks) Done := make(chan bool) go doMasterResult(Done) // concurrency // doWorker(&wg, numWorkers) doWorker(numWorkers) //// 信道的接收和发送默认都是阻塞的,对于发送而言,如果没有接收处理完,阻塞,不会执行下一句 //// buffered channel只有信道缓冲区满后发送信道才阻塞,缓冲区空时接收信道阻塞 //wg.Wait() <-Done // close(chWorker) // 信道的接收和发送默认都是阻塞的,对于发送而言,如果没有接收处理完,阻塞,不会执行下一句 // close(chMaster) // 若所有的goroutine都处于休眠(阻塞状态),main还在等待从管道获取数据 // 则main永远获取不到(信道不会被激活),于是main主动杀死自己,报错: // all goroutines are asleep - deadlock! // 简单来说,主线程在阻塞,但是其他协程由于各种原因也阻塞了。 diff := time.Now().Sub(startTime) fmt.Println("tasks take ", diff.Seconds(), " seconds") }
5.select
select
语句用于在多个发送/接收信道操作中进行选择。
select
语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕,select
会随机地选取其中之一执行。该语法与 switch
类似,所不同的是,这里的每个 case
语句都是信道操作。
在没有 case 准备就绪时,可以执行 select
语句中的默认情况(Default Case)(default立即返回)。
可通过time.After()设置超时处理,这通常用于防止 select
语句一直阻塞。
func main(){ // ch := make(chan string) ch := make(chan string, 2) for { time.Sleep(1000*time.Millisecond) select{ // case 5: // must channel // fmt.Println("not channel case") case ch <- "select case: send": fmt.Println("select case: send ") case v := <-ch: fmt.Println("received value: ", v) return default: fmt.Println("no value received") } } }
两种结果:
$ go run select1.go select case: send select case: send received value: select case: send wang@ubuntu-wang:~/repository/gorepo/golang/concurrency$ go run select1.go select case: send received value: select case: send
package main import ( "fmt" "time" ) func server1(ch chan string) { time.Sleep(6 * time.Second) ch <- "from server1" } func server2(ch chan string) { time.Sleep(3 * time.Second) ch <- "from server2" } func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) select { case s1 := <-output1: fmt.Println(s1) case s2 := <-output2: fmt.Println(s2)
case <- time.After(time.Second * 5)
fmt.Println("Timeout")
// default:
// fmt.Println("No one returned") } }
select应用:假设我们有一个关键性应用,需要尽快地把输出返回给用户。这个应用的数据库复制并且存储在世界各地的服务器上。我们向两台服务器发送请求,并使用 select
语句等待相应的信道发出响应。select
会选择首先响应的服务器,而忽略其它的响应。使用这种方法,我们可以向多个服务器发送请求,并给用户返回最快的响应了。
package main func main() { select {} }
select
语句没有任何 case,因此它会一直阻塞,导致死锁。该程序会触发 panic。
注:退出select用break,当select与for同时使用时,break仅退出select,此时要退出for循环需要使用label(LOOP: for{select...})
6.mutex
Mutex 用于提供一种加锁机制(Locking Mechanism),可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。
var mutex sync.Mutex mutex.Lock() x = x + 1 mutex.Unlock()
信道处理竞态条件(ch = make(chan bool, 1))
ch <- true x = x + 1 <- ch
当 Go 协程需要与其他协程通信时,可以使用信道。而当只允许一个协程访问临界区时,可以使用 Mutex。
7. once
sync.Once可以控制函数只能被调用一次,不会被多次重复调用。
func (o *Once) Do(f func())
package main import ( "fmt" "sync" ) type Watchers struct { devices map[string]string } var ( wcOnce sync.Once watchers *Watchers ) func newWatchers() *Watchers { wcOnce.Do(func() { watchers = &Watchers{devices: map[string]string{"dtu": "modbus", "capture": "wv"}} }) return watchers } func main() { watchers = newWatchers() fmt.Println(watchers) }
// &{map[capture:wv dtu:modbus]}
sync.Once.Do(f func())能保证once只执行一次,无论之后是否更换once.Do(xx)里的方法。
package main import ( "fmt" "sync" ) func main(){ var once sync.Once once.Do(func(){ fmt.Println("Test sync Once") }) }
8. 管理Concurrency的三种方式
WaitGroup:需要将单一个工作分解成多个子任务,等到全部完成后,才能进行下一步,这时候用 WaitGroup 最适合了。
Channel + Select:Channel 只能用在比较单纯的 Goroutine 情况下,如果要管理多个 Goroutine,建议还是走context 会比较适合。
Context:如果您想一次控制全部的 Goroutine,相信用 context 会是最适合不过的,当然 context 不只有这特性,详细可以参考『用 10 分钟了解 Go 语言 context package 使用场景及介绍』。多层级groutine之间的信号传播(包括元数据传播,取消信号传播、超时控制等)。
9. errgroup
golang.org/x/sync/errgroup https://pkg.go.dev/golang.org/x/sync/errgroup
// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package errgroup provides synchronization, error propagation, and Context // cancelation for groups of goroutines working on subtasks of a common task. package errgroup import ( "context" "sync" ) // A Group is a collection of goroutines working on subtasks that are part of // the same overall task. // // A zero Group is valid and does not cancel on error. type Group struct { cancel func() wg sync.WaitGroup errOnce sync.Once err error } // WithContext returns a new Group and an associated Context derived from ctx. // // The derived Context is canceled the first time a function passed to Go // returns a non-nil error or the first time Wait returns, whichever occurs // first. func WithContext(ctx context.Context) (*Group, context.Context) { ctx, cancel := context.WithCancel(ctx) return &Group{cancel: cancel}, ctx } // Wait blocks until all function calls from the Go method have returned, then // returns the first non-nil error (if any) from them. func (g *Group) Wait() error { g.wg.Wait() if g.cancel != nil { g.cancel() } return g.err } // Go calls the given function in a new goroutine. // // The first call to return a non-nil error cancels the group; its error will be // returned by Wait. func (g *Group) Go(f func() error) { g.wg.Add(1) go func() { defer g.wg.Done() if err := f(); err != nil { g.errOnce.Do(func() { g.err = err if g.cancel != nil { g.cancel() } }) } }() }
Package errgroup provides synchronization, error propagation, and Context cancelation for groups of goroutines working on subtasks of a common task.
type Group struct { // contains filtered or unexported fields }
A Group is a collection of goroutines working on subtasks that are part of the same overall task.
A zero Group is valid and does not cancel on error.
errgroup使用的两种方式:
g := &errgroup.Group{}
g, gCtx := errgroup.WithContext(context.Background())
// WithContext returns a new Group and an associated Context derived from ctx. //The derived Context is canceled the first time a function passed to Go returns a non-nil error // or the first time Wait returns, whichever occurs first. func WithContext(ctx context.Context) (*Group, context.Context) // Go calls the given function in a new goroutine. // The first call to return a non-nil error cancels the group; its error will be returned by Wait. func (g *Group) Go(f func() error) // Wait blocks until all function calls from the Go method have returned, // then returns the first non-nil error (if any) from them. func (g *Group) Wait() error
注:在g.Go()的goroutine中不使用Context.Done()时,Group中任一goroutine退出时g.Wait()不会返回(Wait()等Group中所有Goroutine退出)。
若要达到Group中任一goroutine退出,整个Group退出,需要配合Context.Done()使用。
bilibili中对errgroup进一步封装,可实现一个goroutine退出,所有都退出。拓展errgroup == bilibili errgroup
package main import ( "context" "errors" "fmt" "time" "golang.org/x/sync/errgroup" ) func main() { g, gCtx := errgroup.WithContext(context.Background()) g.Go(func() error { time.Sleep(2 * time.Second) fmt.Println("Once goroutine exit...") return errors.New("once goroutine exit") }) g.Go(func() error { LOOP: for { time.Sleep(time.Second) select { case <-gCtx.Done(): break LOOP default: fmt.Println("sleep 1 second...") } } fmt.Println("for goroutine exit...") return errors.New("for goroutine exit") }) if err := g.Wait(); err != nil { fmt.Println("main exit error: ", err) } } /////////// sleep 1 second... Once goroutine exit... for goroutine exit... main exit error: once goroutine exit
上例中若第二个goroutine中不使用gCtx.Done()则第二个goroutine不退出,wait()不返回。
package main import ( "fmt" "log" "net/http" "sync" "time" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" ) type Product struct { Username string `json:"username" binding:"required"` Name string `json:"name" binding:"required"` Category string `json:"category" binding:"required"` Price int `json:"price" binding:"gte=0"` Description string `json:"description"` CreatedAt time.Time `json:"createdAt"` } type productHandler struct { sync.RWMutex products map[string]Product } func newProductHandler() *productHandler { return &productHandler{ products: make(map[string]Product), } } func (u *productHandler) Create(c *gin.Context) { u.Lock() defer u.Unlock() // 1. 参数解析 var product Product if err := c.ShouldBindJSON(&product); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 2. 参数校验 if _, ok := u.products[product.Name]; ok { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("product %s already exist", product.Name)}) return } product.CreatedAt = time.Now() // 3. 逻辑处理 u.products[product.Name] = product log.Printf("Register product %s success", product.Name) // 4. 返回结果 c.JSON(http.StatusOK, product) } func (u *productHandler) Get(c *gin.Context) { u.Lock() defer u.Unlock() product, ok := u.products[c.Param("name")] if !ok { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Errorf("can not found product %s", c.Param("name"))}) return } c.JSON(http.StatusOK, product) } func router() http.Handler { router := gin.Default() productHandler := newProductHandler() // 路由分组、中间件、认证 v1 := router.Group("/v1") { productv1 := v1.Group("/products") { // 路由匹配 productv1.POST("", productHandler.Create) productv1.GET(":name", productHandler.Get) } } return router } func main() { var eg errgroup.Group // 一进程多端口 insecureServer := &http.Server{ Addr: ":8080", Handler: router(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } secureServer := &http.Server{ Addr: ":8443", Handler: router(), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } eg.Go(func() error { err := insecureServer.ListenAndServe() if err != nil && err != http.ErrServerClosed { log.Fatal(err) } return err }) eg.Go(func() error { err := secureServer.ListenAndServeTLS("server.crt", "server.key") if err != nil && err != http.ErrServerClosed { log.Fatal(err) } return err }) if err := eg.Wait(); err != nil { log.Fatal(err) } }
参考:
1. https://studygolang.com/subject/2
2. Go并发编程实践
3. 深入Go并发编程研讨课 https://github.com/akkagao/dive-to-gosync-workshop
4. 课程带你通过一个真实的线上日志监控系统学习Golang以及并发的编程思想 慕课网https://m.imooc.com/
5. 干货 all goroutines are asleep - deadlock 详尽案例分析
6. 在 Go 语言中管理 Concurrency 的三种方式 WaitGroup、channel+select、Context
7. 你知道几种Go并发控制方式? WaitGroup、channel+select、Context