geektutu / blog

极客兔兔的博客,Coding Coding 创建有趣的开源项目。
https://geektutu.com
Apache License 2.0
167 stars 21 forks source link

控制协程(goroutine)的并发数量 | Go 语言高性能编程 | 极客兔兔 #116

Open geektutu opened 3 years ago

geektutu commented 3 years ago

https://geektutu.com/post/hpg-concurrency-control.html

Go 语言/golang 高性能编程,Go 语言进阶教程,Go 语言高性能编程(high performance go)。本文介绍了 goroutine 协程并发控制,避免并发过高,大量消耗系统资源,导致程序崩溃或卡顿,影响性能。主要通过 2 种方式控制,一是使用 channel 的缓冲区,二是使用第三方协程池,例如 tunny 和 ants。同时介绍了使用 ulimit 和虚拟内存(virtual memory)提高资源上限的技巧。

kele1997 commented 3 years ago
// main_chan.go
func main() {
    var wg sync.WaitGroup
    ch := make(chan struct{}, 3)
    for i := 0; i < 10; i++ {
-       ch <- struct{}{}
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
+           ch <- struct{}{}
            log.Println(i)
            time.Sleep(time.Second)
            <-ch
        }(i)
    }
// 这里我们可以做其他的事情
    log.Println("do other thing")
    wg.Wait()
}

在其他地方看到控制协程的时候, ch <- struct{}{} 有放在协程内部的。
想了想,作者最终的实现效果是每次只开 3 个协程,想开新协程时会阻塞住整个代码。(PS: 而且代码里面的 wg 每 Add 1, 就 Done 1,实际上最后 Wait 的只有最后 3 个协程)
而将 ch <- struct{}{} 放在协程里面时,会将所有协程创建,但只有前3个可以继续执行,而其他的协程会阻塞住。这种实现的好处在于代码可以继续执行下去。

话说不会是作者笔误写错了吧? :smile:

geektutu commented 3 years ago

@kele1997 你的质疑是有道理的,不过 ch <- struct{}{} 放到协程内部的话,子协程还是先创建了,channel 只是阻塞了协程内部的执行,达不到限制协程数量的目的。

所以的话,如果需要不阻塞,后面还能继续干活的话,就可以把整个部分,再包装成一个异步的协程。这样协程数量就是 3+1。

比较常见的,比如 HTTP 服务,先启动一个异步的后台协程用于请求数据库,在这个异步协程里用 channel 接收任务,接收后再启动子协程处理,每个 API 请求进来,通过 channel 告知这个后台协程处理。通过设置 channel 的 buffer 大小,就可以控制后台协程并发创建的子协程的数量。

CrazyMouse commented 3 years ago

go pool.Process(i) 这个地方需要用到并发么?是不是去掉go 就行?还有就是,这块是不是得用waitgroup来防止主线程退出?

dablelv commented 2 years ago

2^31 次方约为 20 亿哈,不是 2 亿。

ShiMaRing commented 2 years ago

@kele1997

// main_chan.go
func main() {
  var wg sync.WaitGroup
  ch := make(chan struct{}, 3)
  for i := 0; i < 10; i++ {
-     ch <- struct{}{}
      wg.Add(1)
      go func(i int) {
          defer wg.Done()
+           ch <- struct{}{}
          log.Println(i)
          time.Sleep(time.Second)
          <-ch
      }(i)
  }
// 这里我们可以做其他的事情
    log.Println("do other thing")
  wg.Wait()
}

在其他地方看到控制协程的时候, ch <- struct{}{} 有放在协程内部的。
想了想,作者最终的实现效果是每次只开 3 个协程,想开新协程时会阻塞住整个代码。(PS: 而且代码里面的 wg 每 Add 1, 就 Done 1,实际上最后 Wait 的只有最后 3 个协程)
而将 ch <- struct{}{} 放在协程里面时,会将所有协程创建,但只有前3个可以继续执行,而其他的协程会阻塞住。这种实现的好处在于代码可以继续执行下去。

话说不会是作者笔误写错了吧? :smile:

<-ch放defer语句中会比较好吧,就算panic了也能够释放struct{}{}

fanfansong commented 1 year ago

@kele1997 没有错,这里本来就是为了限制携程数量,如果写在里面,那么就是会创建无数个协程序,会消耗系统内存或者其他资源达到资源上限。

kuankuanlv commented 1 year ago

这第三方库实现的pool好..别扭啊。1:task是固定的,正常应该支持任意函数;2:对外暴露的提交api不合理,提交的时候为什么在外部使用go pool.procress(foo)。

正常来讲三个方法足够,清晰简单,调用方没那么多心智负担: 1,new(routineSize int,queueSieze int)创建方法指定两个size 2,submit(task taskType),提交后就放到任务对立里直接返回 3,shutdown(),不再接收新任务,等待队列任务执行完成后关闭

kuchaguangjie commented 1 year ago

关于 第三方 goroutine pool. https://github.com/Jeffail/tunny 这个 好像几年没 更新了. https://github.com/alitto/pond 这个 比较新, 但是 貌似 挺好用.