goroutine pool,WaitGroup,chan 示例
服务端高并发编程经常需要写很多goroutine来服务每一个连接,如何正确使用goroutine池是又拍云的工程师们需要考虑的问题,今天这篇文章,分享给同样需要使用go语言的小伙伴们。 文/陶克路
本文转载自:http://legendtkl.com/
引言 在上文中,我说到golang的原生http server处理client的connection的时候,每个connection起一个goroutine,这是一个相当粗暴的方法。为了感受更深一点,我们来看一下go的源码。先定义一个最简单的http server如下。 func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello there!\n")
} func main(){
http.HandleFunc("/", myHandler) // 设置访问路由
log.Fatal(http.ListenAndServe(":8080", nil))
}
(如不能看全部代码请往左滑,下同) 从入口http.ListenAndServe函数跟进去。
// file: net/http/server.go
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
} func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
} func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
...
for {
rw, e := l.Accept()
if e != nil {
// error handle
return e
}
tempDelay = 0
c, err := srv.newConn(rw)
if err != nil {
continue
}
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve()
}
}
首先net.Listen负责监听网络端口,rw, e := l.Accept()则从网络端口中取出TCP连接,然后go c.server()则对每一个TCP连接起一个goroutine来处理。我还说到fasthttp这个网络框架性能要比原生的net/http性能要好,其中一个原因就是因为使用了goroutine pool。那么问题来了,如果要我们自己去实现一个goroutine pool,该怎么去实现呢?我们先来实现一个最简单的。 弱鸡版 golang中的goroutine通过go来启动,goroutine资源和临时对象池不一样,不能放回去再取出来。所以goroutine应该是一直运行着的。需要的时候就运行,不需要的时候就阻塞,这样对其他的goroutine的调度影响也不是很大。而goroutine的任务可以通过channel来传递就ok了。很简单的弱鸡版本就出来了,如下。
func Gopool() {
start := time.Now()
wg := new(sync.WaitGroup)
data := make(chan int, 100)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
for _ = range data {
fmt.Println("goroutine:", n, i)
}
}(i)
}
for i := 0; i < 10000; i++ {
data <- i
}
close(data)
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
上面的代码中还做了程序运行时间统计。作为对比,下面是一个没有使用pool的版本。
func Nopool() {
start := time.Now()
wg := new(sync.WaitGroup)
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
//fmt.Println("goroutine", n)
}(i)
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
最后运行时间对比,使用了goroutine pool的代码运行时间约为没有使用pool的代码的2/3。当然这么测试还是略显粗糙了。我们下面使用reflect那篇文章里面介绍的go benchmark testing的方式测试,测试代码如下(去掉了很多无关代码)。
package pool import (
"sync"
"testing"
) func Gopool() {
wg := new(sync.WaitGroup)
data := make(chan int, 100) for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
for _ = range data {
}
}(i)
} for i := 0; i < 10000; i++ {
data <- i
}
close(data)
wg.Wait()
} func Nopool() {
wg := new(sync.WaitGroup) for i := 0; i < 10000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
}(i)
}
wg.Wait()
} func BenchmarkGopool(b *testing.B) {
for i := 0; i < b.N; i++ {
Gopool()
}
} func BenchmarkNopool(b *testing.B) {
for i := 0; i < b.N; i++ {
Nopool()
}
}
最终的测试结果如下,使用了goroutine pool的代码执行时间确实更短。 $ go test -bench='.' gopool_test.go
BenchmarkGopool-8 500 2596750 ns/op
BenchmarkNopool-8 500 3604035 ns/op
PASS
升级版 对于一个好的线程池,我们往往有更多的需求,一个最迫切的需求是能自定义goroutine运行的函数。函数无非就是函数地址和函数参数。如果要传入的函数形式不一样(形参或者返回值不一样)怎么办?一个比较简单的方法是引入反射。
type worker struct {
Func interface{}
Args []reflect.Value
}
func main() {
var wg sync.WaitGroup
channels := make(chan worker, 10)
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for ch := range channels {
reflect.ValueOf(ch.Func).Call(ch.Args)
}
}()
}
for i := 0; i < 100; i++ {
wk := worker{
Func: func(x, y int) {
fmt.Println(x + y)
},
Args: []reflect.Value{reflect.ValueOf(i), reflect.ValueOf(i)},
}
channels <- wk
}
close(channels)
wg.Wait()
}
但是引入反射又会引入性能问题。本来goroutine pool就是为了解决性能问题,然而现在又引入了新的性能问题。那么怎么办呢?闭包。
type worker struct {
Func func()
}
func main() {
var wg sync.WaitGroup
channels := make(chan worker, 10)
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for ch := range channels {
//reflect.ValueOf(ch.Func).Call(ch.Args)
ch.Func()
}
}()
}
for i := 0; i < 100; i++ {
j := i
wk := worker{
Func: func() {
fmt.Println(j + j)
},
}
channels <- wk
}
close(channels)
wg.Wait()
}
这里值得注意的一点是golang的闭包用不好容易把自己代入坑,而理解闭包一个很关键的点就是对对象的引用而不是复制。这里只是goroutine pool 实现的一个精简版,真正实现的时候还有很多细节需要考虑,比如设置一个stop channel用来停止pool,但是goroutine pool的核心就在于这个地方。 goroutine池和CPU核的关系 那么goroutine池里面goroutine数目和核数有没有关系呢?这个其实要分情况讨论。 1.goroutine池跑不满 这也就意味着channel data里面一有数据就会被goroutine拿走,这样的话当然只能你CPU能调度的过来就行,也就是池子里的goroutine数目和CPU核数是最优的。经测试,确实是这样。 2.channel data有数据阻塞 这意思是说goroutine是不够用的,如果goroutine的运行任务不是CPU密集型的(大部分情况都不是),而只是IO阻塞,这个时候一般goroutine数目在一定范围内是越多越好,当然范围在什么地方就要具体情况具体分析了。
goroutine pool,WaitGroup,chan 示例的更多相关文章
- 如果裸写一个goroutine pool
引言 在上文中,我说到golang的原生http server处理client的connection的时候,每个connection起一个goroutine,这是一个相当粗暴的方法.为了感受更深一点, ...
- 通过 Channel 实现 Goroutine Pool
最近用到了 Go 从 Excel 导数据到服务器内部 用的是 http 请求 但是发现一个问题 从文件读取之后 新开 Goroutine 会无限制新增 导致全部卡在初始化请求 于是乎就卡死了 问题模拟 ...
- fasthttp 的 goroutine pool 实现探究
引言 fasthttp是一个非常优秀的web server框架,号称比官方的net/http快10倍以上.fasthttp用了很多黑魔法.俗话说,源码面前,了无秘密,我们今天通过源码来看一看她的gor ...
- 3.2 go WaitGroup代码示例
sync.WaitGroup提供了一种安全的多协程处理方法,内部使用race.atomic来处理,避免了资源竞争及锁的产生. 主要的方法有Add.Done.Wait,可以等待一组协程全部执行完毕后,主 ...
- golang的sync.WaitGroup使用示例
下面一段代码 len(m) 不一定会打印为 10,为什么?.如果想要 len(m) 打印为 10,应该怎么修改代码? func main() { const N = 10 m := make(map[ ...
- go中控制goroutine数量
控制goroutine数量 前言 控制goroutine的数量 通过channel+sync 使用semaphore 线程池 几个开源的线程池的设计 fasthttp中的协程池实现 Start Sto ...
- 使goroutine同步的方法总结
前言: 在前面并发性能对比的文章中,我们可以看到Golang处理大并发的能力十分强劲,而且开发也特别方便,只需要用go关键字即可开启一个新的协程. 但当多个goroutine同时进行处理的时候,就会遇 ...
- go语言之行--golang核武器goroutine调度原理、channel详解
一.goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心.goroutine使用方式非常的简单,只需使用go关键字 ...
- Goroutine并发调度模型深度解析之手撸一个协程池
golanggoroutine协程池Groutine Pool高并发 并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题:Go语言作为一个出道以来就自带 『高并发』光环 ...
随机推荐
- BZOJ1902: Zju2116 Christopher
$n \leq 10^{100}$,问$C_n^m,0<=m<=n$有多少是质数$p \leq 1e7$的倍数. 一样,套高精度的题,只有战胜他才能鄙视他. 但是我TM被他鄙视了一上午!! ...
- 批处理BAT替换与截取字符串的用法t1=%a:~3%是什么意思
在bat编写中,我们经常越到t1=%a:~3%之类的代码,这里简单介绍下用法,需要的朋友可以参考下: 一.替换用法 例 @echo off set a=belcome to CMD borld! ...
- 查看mysql库中所有表的大小和记录数
查看mysql库中所有表的大小和记录数 ,), 'MB') as total_size FROM information_schema.TABLES WHERE TABLE_SCHEMA='datab ...
- php——两种无限级分类
/** * 无级递归分类 TP框架 * @param int $assortPid 要查询分类的父级id * @param mixed $tag 上下级分类之间的分隔符 * @return strin ...
- DOM对象之查找标签&属性操作
HTML DOM (文档对象模型) DOM(Document Object Model)是一套对文档的内容进行抽象和概念化的方法. JavaScript对DOM进行了实现,对应于JavaScript中 ...
- 词法分析器 /c++实现
#include<iostream> #include<string> #include<vector> #include<map> #include& ...
- .NET Core 3.0之创建基于Consul的Configuration扩展组件
写在前面 经过前面三篇关于.NET Core Configuration的文章之后,本篇文章主要讨论如何扩展一个Configuration组件出来.如果前面三篇文章没有看到,可以点击如下地址访问 .N ...
- python多线程(一)
原文:http://www.pythonclub.org/python-basic/threading 一.python多线程thread和threading实现 python是支持多线程的,并且是n ...
- Dfs学习经验(纸上运行理解DFS)【两道题】
首先我想吐槽的是,在CSDN上搞了好久还是不能发博客,就是点下发表丝毫反应都没有的,我稍微百度了几次还是没有找到解决方法,在CSDN的BBS上也求助过管理员但是没有收到答复真是烦躁,导致我新生入学以来 ...
- 前端微服务-面向web平台级应用的设计
从去年开始,前端领域就出现了一个‘微应用’的名词,说的是前端架构的一种设计思路,业内都把它和后端的微服务进行类比,当时忙于公司的项目.没有静下心来好好了解,现在项目结束,再加上最近看的几篇关于前端微服 ...