GO语言(golang)如何工作高并发,举例
GO语言在WEB开发领域的使用越来越广泛。 Hired 发布的一份《2019 软件工程师状态》报告发现,拥有围棋经验的候选人是迄今为止最有吸引力的。平均每个求职者会接受 9 次面试。
想要学围棋,最基本的就是要了解高并发是怎么走的。
那么高并发是什么?
高并发(高并发)是设计互联网分布式系统架构时要考虑的因素之一。通常是指系统在规划时可以同时并行处理多个请求。
据严格介绍,单核CPU无法实现并行。严格表示,只有多核CPU才能实现并行,因为一个CPU一次只能做一件事。那么为什么单核CPU能够高并发呢?这是OS进程的线程调度的切换实现,看起来是并行处理。因此,只要有足够的线程,就可以处理C1K C10K请求,但线程数量受到操作系统内存等资源的限制。每个线程必须分配8M的堆栈内存,无论是否正在使用。每个php-fpm大约需要20M内存。因此,目前具有线程的Java比仅具有进程的PHP具有更强的并发处理能力。当然,软件处理性能不仅仅与内存有关,还与是否阻塞、异步处理、CPU等有关。作为单线程模型,Nginx 可以处理数万甚至数十万的并发请求。关于 Nginx 有几个主题。
我们再多谈谈Go,那么有没有可能有一种语言,使用比线程更小的处理单元,占用更少的内存,因此它的并行处理能力可以更高。于是 Google 就这么做了,并提出了 golang 语言。 Golang在语言层面支持高并发。
为什么可以走高并发?
goroutine是Go并行设计的核心。最终,goroutine 实际上是一个协程,但比线程小。下面五六个线程就可以体现出几十个goroutine。 Go 语言帮助实现这些 goroutine 之间的内存共享。 Goroutine 的执行需要很少的堆栈内存(大约 4-5 KB),并且自然地根据相应的数据进行扩展。因此,可以同时运行数千个并行任务。 Goroutine 比线程更容易使用、更高效、更轻量。
一些高并发处理方案基本上都是使用协程。 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服务就搭建起来了。然后我们将逐步了解这个网络服务是如何工作的以及如何访问Future。
我们按照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 结构。
接下来,让我们看看 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) }
。我们看到我们传递的自定义响应方法被强制转换为HandlerFunc,所以我们传递的响应方法默认实现了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 } }
将路由和处理后的处理函数存储在 ServeMux.m 的映射表中。映射 muxEntry 结构如上所示。一条路由对应一个handler的处理方法。
让我们看看您要使用的add服务使用TCP协议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)}) }
接下来就是关键代码了,HTTP处理流程 为里面的l.Accept()接受TCP连接请求, 这是GO高并发最关键的一点。每个请求都由单独的 goroutine 执行。 那么之前设置的路由匹配到哪里呢?这个 c.serverde net.Listen("tcp", addr)被制作了。 tcpKeepAliveListener 监听 addr 端口。
serverHandler{c.server❝w,.ServeHTreq(. )。看代码: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)
}
}
c := srv.newConn(rw)
,里面的Conn了保存请求信息(srv、rw)。启动goroutine,将请求的参数传递给c.serve并让goroutine执行。 c.readRequest(ctx)
URI METHOD 等。分析出来,执行
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 的第二个参数,我们是零,因此我们使用DefaultServeMux。我们知道,当我们开始路由时,我们设置了DefaultServeMux,所以在DefaultServeMux中我确保找到请求路由的处理程序,然后执行ServeHTTP。我们已经展示了为什么我们的响应方法有ServeHTTP .流程差不多是这样的.
我们看一下流程图.这些只是GO的冰山一角,还有Redis MySQL连接池.
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。