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的連接池。要熟悉這門語言還是多寫多看,才能掌握好它。靈活熟練的使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章