引言

在上文中,我说到golang的原生http server处理client的connection的时候,每个connection起一个goroutine,这是一个相当粗暴的方法。为了感受更深一点,我们来看一下go的源码。先定义一个最简单的http server如下。

1
2
3
4
5
6
7
8
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函数跟进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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了。很简单的弱鸡版本就出来了,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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的方式测试,测试代码如下(去掉了很多无关代码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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的代码执行时间确实更短。

1
2
3
4
5
$ go test -bench='.' gopool_test.go
BenchmarkGopool-8            500       2596750 ns/op
BenchmarkNopool-8            500       3604035 ns/op
PASS
 

升级版

对于一个好的线程池,我们往往有更多的需求,一个最迫切的需求是能自定义goroutine运行的函数。函数无非就是函数地址和函数参数。如果要传入的函数形式不一样(形参或者返回值不一样)怎么办?一个比较简单的方法是引入反射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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就是为了解决性能问题,然而现在又引入了新的性能问题。那么怎么办呢?闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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的更多相关文章

  1. golang 裸写一个pool池控制协程的大小

    这几天深入的研究了一下golang 的协程,读了一个好文 http://mp.weixin.qq.com/s?__biz=MjM5OTcxMzE0MQ==&mid=2653369770& ...

  2. fasthttp 的 goroutine pool 实现探究

    引言 fasthttp是一个非常优秀的web server框架,号称比官方的net/http快10倍以上.fasthttp用了很多黑魔法.俗话说,源码面前,了无秘密,我们今天通过源码来看一看她的gor ...

  3. goroutine pool,WaitGroup,chan 示例

    服务端高并发编程经常需要写很多goroutine来服务每一个连接,如何正确使用goroutine池是又拍云的工程师们需要考虑的问题,今天这篇文章,分享给同样需要使用go语言的小伙伴们. 文/陶克路 本 ...

  4. 通过 Channel 实现 Goroutine Pool

    最近用到了 Go 从 Excel 导数据到服务器内部 用的是 http 请求 但是发现一个问题 从文件读取之后 新开 Goroutine 会无限制新增 导致全部卡在初始化请求 于是乎就卡死了 问题模拟 ...

  5. python_way ,day11 线程,怎么写一个多线程?,队列,生产者消费者模型,线程锁,缓存(memcache,redis)

    python11 1.多线程原理 2.怎么写一个多线程? 3.队列 4.生产者消费者模型 5.线程锁 6.缓存 memcache redis 多线程原理 def f1(arg) print(arg) ...

  6. 【Python】如何基于Python写一个TCP反向连接后门

    首发安全客 如何基于Python写一个TCP反向连接后门 https://www.anquanke.com/post/id/92401 0x0 介绍 在Linux系统做未授权测试,我们须准备一个安全的 ...

  7. 从头写一个Cucumber测试(二) Cucumber Test

    转载:https://yaowenjie.github.io/%E7%BC%96%E7%A8%8B%E7%9B%B8%E5%85%B3/cucumber-test-part-2 承接上文   前一篇博 ...

  8. springboot自动装配原理,写一个自己的start

    springboot自动装配原理 第一次使用springboot的时候,都感觉很神奇.只要加入一个maven的依赖,写几行配置,就能注入redisTemple,rabbitmqTemple等对象. 这 ...

  9. 手写一个最迷你的Web服务器

    今天我们就仿照Tomcat服务器来手写一个最简单最迷你版的web服务器,仅供学习交流. 1. 在你windows系统盘的F盘下,创建一个文件夹webroot,用来存放前端代码.  2. 代码介绍: ( ...

随机推荐

  1. 有关Java 锁原理

    锁 锁是用来锁东西的,让别人打不开也看不到!在线程中,用这个“锁”隐喻来说明一个线程在“操作”一个目标(如一个变量)的时候,如果变量是被锁住的,那么其他线程就对这个目标既“操作”不了(挂起)也无法看到 ...

  2. Pydev Console中文提示乱码的问题

    1. 像这样的规则内容请这样处理"\u305d\u3093\u306a\u306b"style unicode string : print str.decode("un ...

  3. Webpack的配置与使用

    一.什么是Webpack?     WebPack可以看做是模块打包机.用于分析项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),将 ...

  4. 机器学习(2) - KNN识别MNIST

    代码 https://github.com/s055523/MNISTTensorFlowSharp 数据的获得 数据可以由http://yann.lecun.com/exdb/mnist/下载.之后 ...

  5. linux系统开机流程详解

    今天,我们主要来谈谈计算机系统的启动流程 1.BIOS启动 BIOS是写入到主板上的一个韧体(韧体就是写入到硬件上的一个软件程序).开机的时候,BIOS是计算机系统会主动执行的第一个程序.BIOS主要 ...

  6. HTML DOM对象的属性和方法

    HTML DOM对象的属性和方法 HTML DOM 对象有几种类型: 1.Document 类型 在浏览器中,Document 对象表示整个 HTML 文档. 1.1属性 引用文档的子节点 docum ...

  7. 基于Microsoft Graph打造自己的Timeline应用

    原文链接:https://github.com/chenxizhang/office365dev/blob/e9b5a59cb827841d36692cc4ec52c11d43062e04/docs/ ...

  8. 【转】Javascript错误处理——try…catch

    无论我们编程多么精通,脚本错误怎是难免.可能是我们的错误造成,或异常输入,错误的服务器端响应以及无数个其他原因. 通常,当发送错误时脚本会立刻停止,打印至控制台. 但try...catch语法结构可以 ...

  9. 创建ndarray

    Numpy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器,是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的. 创建数组最简单的方法就 ...

  10. SpringMVC中日期格式的转换

    解决日期提交转换异常的问题 由于日期数据有很多种格式,所以springmvc没办法把字符串转换成日期类型.所以需要自定义参数绑定.前端控制器接收到请求后,找到注解形式的处理器适配器,对RequestM ...