Understanding of high concurrency in golang

Preface

The GO language is used more and more widely in the field of WEB development. Hired's "2019 State of Software Engineers" report pointed out that candidates with Go experience are by far the most attractive. On average, each job seeker will receive 9 interview invitations.
Insert picture description here
To learn go, the most basic thing is to understand how go achieves high concurrency.
So what is high concurrency?

High Concurrency is one of the factors that must be considered in the design of the Internet distributed system architecture. It usually refers to the design to ensure that the system can simultaneously process many requests in parallel.

Strictly speaking, a single-core CPU cannot achieve parallelism. Only a multi-core CPU can achieve strict parallelism, because a CPU can only do one thing at a time. Then why a single-core CPU can achieve high concurrency. This is the operating system process thread scheduling switch execution, which feels parallel processing. So as long as there are enough process threads, C1K C10K requests can be processed, but the number of process threads is limited by resources such as operating system memory. Each thread must allocate 8M stack memory, regardless of whether it is used or not. Each php-fpm needs about 20M of memory. Therefore, the current threaded Java has higher concurrent processing capabilities than the process-only PHP. Of course, the processing power of software is not only related to memory, but also whether it is blocked, whether it is asynchronous processing, CPU, etc. As a single-threaded model, Nginx can handle tens of thousands or even hundreds of thousands of concurrent requests. Nginx talks more about it.
Let's continue to talk about our Go, so can there be a language that uses a smaller processing unit and takes up less memory than threads, so its concurrent processing capabilities can be higher. So Google did this, and there was the golang language. Golang supports high concurrency from the language level.

Why can go achieve high concurrency

Goroutine is the core of Go parallel design. In the final analysis, goroutine is actually a coroutine, but it is smaller than a thread. Dozens of goroutines may be embodied in five or six threads at the bottom. The Go language helps you achieve memory sharing between these goroutines. The execution of goroutine requires very little stack memory (about 4~5KB), and of course it will scale according to the corresponding data. Because of this, thousands of concurrent tasks can be run at the same time. Goroutines are easier to use, more efficient, and lighter than threads.

Some high-concurrency processing solutions basically use coroutines. Openresty also uses lua language coroutines to achieve high-concurrency processing capabilities. PHP's high-performance framework Swoole is currently using PHP coroutines.
The coroutine is lighter and occupies less memory, which is the premise for it to achieve high concurrency.

How to achieve high concurrency in go web development

Learn the HTTP code of go. First create a simple web service.

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)
	}
}

Then compile

go build -o test_web.gobin
./test_web.gobin

Then visit

curl 127.0.0.1:9000
Hello world!

Such a simple WEB service is built. Next, we understand step by step how this Web service works and how to achieve high concurrency.
We follow the http.HandleFunc("/", response) method and look up the code.

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
}

The above is the definition and description of DefaultServeMux. We see the ServeMux structure, which has a read-write lock to handle concurrent use. The muxEntry structure contains handler processing methods and routing strings.
Next we look at what the http.HandleFunc function, which is DefaultServeMux.HandleFunc, does. Let's first look at the second parameter HandlerFunc(handler) of mux.Handle

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)
}

We saw that the custom response method we passed was forced to be converted to the HandlerFunc type, so the response method we passed implemented the ServeHTTP method by default.

We then look at the first parameter of 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
	}
}

Store the route string and the handler function to be processed in the map table of ServeMux.m, the muxEntry structure in the map, as described above, a route corresponds to a handler processing method.
Next, let’s take a look at what http.ListenAndServe(":9000", nil) does

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), is to use the port addr to build a service with TCP protocol. tcpKeepAliveListener monitors the port of addr.
The next step is the key code, HTTP processing

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)
	}
}

l.Accept() in for accepts TCP connection requests, c := srv.newConn(rw) creates a Conn, and Conn saves the information of the request (srv, rw). Start the goroutine, pass the requested parameters to c.serve, and let the goroutine execute it.
This is the most critical point of GO high concurrency. Each request is executed by a separate goroutine.
So where does the previously set route match? It is done by analyzing the URI METHOD etc. in the c.readRequest(ctx) of c.serverde and executing serverHandler{c.server}.ServeHTTP(w, w.req). Look at the code

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)
}

The handler is empty, just the second parameter of ListenAndServe in the project we just started. We are nil, so we go to DefaultServeMux. We know that we set DefaultServeMux to start routing, so in DefaultServeMux I can definitely find the handler corresponding to the requested route, and then execute ServeHTTP. As mentioned earlier, why our response method has the function of ServeHTTP. The process is probably like this.

We look at the flow chart
Insert picture description here

Conclusion

We have basically forgotten the whole working principle of GO's HTTP, and understood why it can achieve high concurrency in WEB development. These are just the tip of the iceberg of GO, as well as the connection pool of Redis MySQL. To be familiar with this language, or to write and read more, can you master it well. Flexible and skilled use.

------------------------------------end
pay attention to high-performance WEB back-end technology together, pay attention to the official account
Insert picture description here

Guess you like

Origin blog.csdn.net/feifeixiang2835/article/details/88261685