golang高并发的深入理解
前言
go语言在web开发领域中的使用越来越广泛,hired 发布的《2019 软件工程师状态》报告中指出,具有 go 经验的候选人是迄今为止最具吸引力的。平均每位求职者会收到9 份面试邀请。
想学习go,最基础的就要理解go是怎么做到高并发的。
那么什么是高并发?
高并发(high concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
严格意义上说,单核的cpu是没法做到并行的,只有多核的cpu才能做到严格意义上的并行,因为一个cpu同时只能做一件事。那为什么是单核的cpu也能做到高并发。这就是操作系统进程线程调度切换执行,感觉上是并行处理了。所以只要进程线程足够多,就能处理c1k c10k的请求,但是进程线程的数量又受到操作系统内存等资源的限制。每个线程必须分配8m大小的栈内存,不管是否使用。每个php-fpm需要占用大约20m的内存。所以目前有线程的java就比只有进程的php的并发处理能力高。当然了,软件的处理能力不仅仅跟内存有关,还有是否阻塞,是否异步处理,cpu等等。nginx作为单线程的模型却可以承担几万甚至几十万的并发请求,nginx的话题说起来也就更多了。
我们继续聊我们的go,那么是不是可以有一种语言使用更小的处理单元,占用内存比线程更小,那么它的并发处理能力就可以更高。所以google就做了这件事,就有了golang语言,golang从语言层面就支持了高并发。
go为什么能做到高并发
goroutine是go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,几十个goroutine可能体现在底层就是五六个线程,go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5kb),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
一些高并发的处理方案基本都是使用协程,openresty也是利用lua语言的协程做到了高并发的处理能力,php的高性能框架swoole目前也在使用php的协程。
协程更轻量,占用内存更小,这是它能做到高并发的前提。
go web开发中怎么做到高并发的能力
学习go的http代码。先创建一个简单的web服务。
package main import ( "fmt" "log" "net/http" ) func response(w http.responsewriter, r *http.request) { fmt.fprintf(w, "hello world!") //这个写入到w的是输出到客户端的 } func main() { http.handlefunc("/", response) err := http.listenandserve(":9000", nil) if err != nil { log.fatal("listenandserve: ", err) } }
然后编译
go build -o test_web.gobin ./test_web.gobin
然后访问
curl 127.0.0.1:9000
hello world!
这样简单的一个web服务就搭建起来。接下来我们一步一步理解这个web服务是怎么运行的,怎么做到高并发的。
我们顺着http.handlefunc("/", response)方法顺着代码一直往上看。
func handlefunc(pattern string, handler func(responsewriter, *request)) { defaultservemux.handlefunc(pattern, handler) } var defaultservemux = &defaultservemux var defaultservemux servemux type servemux struct { mu sync.rwmutex//读写锁。并发处理需要的锁 m map[string]muxentry//路由规则map。一个规则一个muxentry hosts bool //规则中是否带有host信息 } 一个路由规则字符串,对应一个handler处理方法。 type muxentry struct { h handler pattern string }
上面是defaultservemux的定义和说明。我们看到servemux结构体,里面有个读写锁,处理并发使用。muxentry结构体,里面有handler处理方法和路由字符串。
接下来我们看下,http.handlefunc函数,也就是defaultservemux.handlefunc做了什么事。我们先看mux.handle第二个参数handlerfunc(handler)
func (mux *servemux) handlefunc(pattern string, handler func(responsewriter, *request)) { mux.handle(pattern, handlerfunc(handler)) } type handler interface { servehttp(responsewriter, *request) // 路由实现器 } type handlerfunc func(responsewriter, *request) func (f handlerfunc) servehttp(w responsewriter, r *request) { f(w, r) }
我们看到,我们传递的自定义的response方法被强制转化成了handlerfunc类型,所以我们传递的response方法就默认实现了servehttp方法的。
我们接着看mux.handle第一个参数。
func (mux *servemux) handle(pattern string, handler handler) { mux.mu.lock() defer mux.mu.unlock() if pattern == "" { panic("http: invalid pattern") } if handler == nil { panic("http: nil handler") } if _, exist := mux.m[pattern]; exist { panic("http: multiple registrations for " + pattern) } if mux.m == nil { mux.m = make(map[string]muxentry) } mux.m[pattern] = muxentry{h: handler, pattern: pattern} if pattern[0] != '/' { mux.hosts = true } }
将路由字符串和处理的handler函数存储到servemux.m 的map表里面,map里面的muxentry结构体,上面介绍了,一个路由对应一个handler处理方法。
接下来我们看看,http.listenandserve(":9000", nil)
做了什么
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)}) }
net.listen("tcp", addr)
,就是使用端口addr用tcp协议搭建了一个服务。tcpkeepalivelistener就是监控addr这个端口。
接下来就是关键代码,http的处理过程
func (srv *server) serve(l net.listener) error { defer l.close() if fn := testhookserverserve; fn != nil { fn(srv, l) } var tempdelay time.duration // how long to sleep on accept failure if err := srv.setuphttp2_serve(); err != nil { return err } srv.tracklistener(l, true) defer srv.tracklistener(l, false) basectx := context.background() // base is always background, per issue 16220 ctx := context.withvalue(basectx, servercontextkey, srv) for { rw, e := l.accept() if e != nil { select { case <-srv.getdonechan(): return errserverclosed default: } if ne, ok := e.(net.error); ok && ne.temporary() { if tempdelay == 0 { tempdelay = 5 * time.millisecond } else { tempdelay *= 2 } if max := 1 * time.second; tempdelay > max { tempdelay = max } srv.logf("http: accept error: %v; retrying in %v", e, tempdelay) time.sleep(tempdelay) continue } return e } tempdelay = 0 c := srv.newconn(rw) c.setstate(c.rwc, statenew) // before serve can return go c.serve(ctx) } }
for里面l.accept()接受tcp的连接请求,c := srv.newconn(rw)
创建一个conn,conn里面保存了该次请求的信息(srv,rw)。启动goroutine,把请求的参数传递给c.serve,让goroutine去执行。
这个就是go高并发最关键的点。每一个请求都是一个单独的goroutine去执行。
那么前面设置的路由是在哪里匹配的?是在c.serverde的c.readrequest(ctx)
里面分析出uri method等,执行serverhandler{c.server}.servehttp(w, w.req)
做的。看下代码
func (sh serverhandler) servehttp(rw responsewriter, req *request) { handler := sh.srv.handler if handler == nil { handler = defaultservemux } if req.requesturi == "*" && req.method == "options" { handler = globaloptionshandler{} } handler.servehttp(rw, req) }
handler为空,就我们刚开始项目中的listenandserve第二个参数。我们是nil,所以就走defaultservemux,我们知道开始路由我们就设置的是defaultservemux,所以在defaultservemux里面我一定可以找到请求的路由对应的handler,然后执行servehttp。前边已经介绍过,我们的reponse方法为什么具有servehttp的功能。流程大概就是这样的。
我们看下流程图
结语
我们基本已经学习忘了go 的http的整个工作原理,了解到了它为什么在web开发中可以做到高并发,这些也只是go的冰山一角,还有redis mysql的连接池。要熟悉这门语言还是多写多看,才能掌握好它。灵活熟练的使用。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。
上一篇: vbs中关于计算机名的相关讨论
下一篇: HTML5 拖拽复制功能的实现