blog icon indicating copy to clipboard operation
blog copied to clipboard

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

Open geektutu opened this issue 3 years ago • 8 comments

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

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

geektutu avatar Dec 20 '20 17:12 geektutu

// 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:

kele1997 avatar Jan 05 '21 09:01 kele1997

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

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

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

geektutu avatar Jan 06 '21 02:01 geektutu

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

CrazyMouse avatar Jan 23 '21 09:01 CrazyMouse

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

dablelv avatar Mar 06 '22 12:03 dablelv

@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{}{}

ShiMaRing avatar Jul 16 '22 14:07 ShiMaRing

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

fanfansong avatar Jan 30 '23 08:01 fanfansong

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

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

kuankuanlv avatar Apr 24 '23 09:04 kuankuanlv

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

kuchaguangjie avatar Jun 23 '23 07:06 kuchaguangjie