In-depth golang of---goroutine concurrency control and communication

Source: Internet
Author: User

When developing go programs, often need to use goroutine concurrent processing tasks, sometimes these goroutine are independent of each other, and sometimes, multiple goroutine often need to synchronize and communicate. In another case, the master Goroutine needs to control the sub-goroutine it belongs to, summing up, to achieve synchronization and communication between multiple goroutine are roughly:

    • Global shared variables
    • Channel communication (CSP model)
    • Context Pack

This article through Goroutine synchronization and communication of a typical scenario-notification sub-goroutine exit run, to further explain the Golang control concurrency.

Notifies multiple child goroutine to quit running

Goroutine as the Go language concurrency weapon, not only strong performance and ease of use: Only a keyword go to the common function can be executed concurrently, and Goroutine occupy a small amount of memory (a goroutine of only 2KB of memory), So when developing go program, many developers often use this concurrency tool, independent concurrency task is relatively simple, only need to use the GO keyword modifier function can enable a goroutine directly run, but the actual concurrency scenario is often required to synchronize and communicate between the process, As well as the precise control of the goroutine start and end, one of the typical scenarios is that the master process notifies all child goroutine gracefully to exit the run.

Since the exit mechanism of the goroutine is designed to be, goroutine exit can only be controlled by itself, not forcing the end of the goroutine from the outside. There are only two exceptions: the end of the main function or the end of the program crash; therefore, to achieve the start and end of the master Process Control sub-goroutine, other tools must be implemented.

Methods for controlling concurrency

The way to control concurrency can be broadly divided into the following three categories:

    • Global shared variables
    • Channel Communication
    • Context Pack

Global shared variables

This is the simplest way to implement control concurrency, with the following steps:

    1. declares a global variable;
    2. All sub-goroutine share this variable and constantly poll this variable to check for updates;
    3. Change the global variable in the main process;
    4. The sub-goroutine detects a global variable update and executes the appropriate logic.

Examples are as follows:

package mainimport (    "fmt"    "time")func main() {    running := true    f := func() {        for running {            fmt.Println("sub proc running...")            time.Sleep(1 * time.Second)        }        fmt.Println("sub proc exit")    }    go f()    go f()    go f()    time.Sleep(2 * time.Second)    running = false    time.Sleep(3 * time.Second)    fmt.Println("main proc exit")}

The advantage of global variables is simple and convenient, do not need too many complicated operation, through a variable can control all sub-goroutine start and end, the disadvantage is that the function is limited, because of the schema, the global variable can only be read one write, otherwise there will be data synchronization problem, Of course, you can also solve this problem by locking the global variable, but that adds complexity, which is not suitable for communication between sub-goroutine, because the global variable can pass very little information, and the main process cannot wait for all child goroutine to exit. Because this approach can only be one-way notifications, this method is only suitable for very simple logic and a less concurrent scenario, and once the logic is slightly more complex, this approach is a bit stretched.

Channel Communication

Another more general and flexible way to control concurrency is to use the channel for communication.
First, let's take a look at what the Channel:channel in Golang is a core type in go, and you can think of it as a conduit through which the core unit can send or receive data for communication (communication).
To understand the channel, you first know the CSP model:

CSP is the abbreviation for communicating sequential process, which can be called the communication sequence process and is a concurrent programming model, presented by Tony Hoare in 1977. In simple terms, the CSP model consists of the entities (threads or processes) that are executed concurrently, and the entities communicate by sending messages, which are either channels or channel. The key to the CSP model is to focus on the channel, not the entity that sends the message. The Go language implements the CSP section theory, goroutine corresponds to the entities that are executed concurrently in the CSP, and the channel corresponds to the channel in the CSP.
That is, CSPs describe a concurrency model in which multiple process uses a channel for communication, the channel link is usually anonymous, and message delivery is usually synchronous (as opposed to the Actor Model).

First look at the sample code:

 package mainimport ("FMT" "OS" "Os/signal" "Sync" "Syscall" "Time") Func consumer ( Stop <-chan bool) {for {select {case <-stop:fmt. Println ("Exit Sub Goroutine") Return DEFAULT:FMT. Println ("Running ...") time. Sleep (Time.millisecond)}}}func main () {stop: = make (chan bool) var wg sync. Waitgroup//Spawn Example Consumers for I: = 0; I < 3; i++ {WG. ADD (1) Go func (Stop <-chan bool) {defer WG. Done () consumer (Stop)} (stop)} waitforsignal () Close (stop) fmt.        PRINTLN ("Stopping all jobs!") Wg. Wait ()}func waitforsignal () {sigs: = Make (chan os. Signal) Signal. Notify (sigs, OS. INTERRUPT) signal. Notify (SIGs, Syscall. SIGTERM) <-sigs}  

This can be achieved gracefully waiting for all sub-goroutine to complete after the main process to end the exit, with the help of the standard library sync Waitgroup, this is a way to control concurrency, you can achieve a multi-goroutine wait, the official document is described as:

A waitgroup waits for a collection of goroutines to finish. The main goroutine calls ADD to set the number of Goroutines to wait for.
Then each of the goroutines runs and calls do when finished. At the same time, Wait can is used to block until all Goroutines has finished.

In short, it implements a similar counter structure in the source code, recording each of the processes registered in it, and then every time after the completion of the task of the process to go to it to log off, and then in the main processes can wait until all the work to complete the task exit.
Steps to use:

    1. Create an instance of Waitgroup WG;
    2. When each goroutine is started, the WG is called. ADD (1) registration;
    3. Call the WG before exiting after each goroutine completes the task. Done () logoff.
    4. The WG is called where all goroutine are waiting. Wait () blocks the process, knowing that all goroutine are completing the task called WG. After done () is logged off, the Wait () method returns.

The sample program is a typical use of a golang select+channel, and let's take a little deeper into this typical usage:

Channel

First understand the channel, which can be understood as the pipeline, its main function point is:

    1. Queue Storage Data
    2. Blocking and Waking Goroutine

The channel implementation is concentrated in the file Runtime/chan.go, the channel underlying data structure is this:

type hchan struct {    qcount   uint           // 队列中数据个数    dataqsiz uint           // channel 大小    buf      unsafe.Pointer // 存放数据的环形数组    elemsize uint16         // channel 中数据类型的大小    closed   uint32         // 表示 channel 是否关闭    elemtype *_type // 元素数据类型    sendx    uint   // send 的数组索引    recvx    uint   // recv 的数组索引    recvq    waitq  // 由 recv 行为(也就是 <-ch)阻塞在 channel 上的 goroutine 队列    sendq    waitq  // 由 send 行为 (也就是 ch<-) 阻塞在 channel 上的 goroutine 队列    // lock protects all fields in hchan, as well as several    // fields in sudogs blocked on this channel.    //    // Do not change another G's status while holding this lock    // (in particular, do not ready a G), as this can deadlock    // with stack shrinking.    lock mutex}

From the source can be seen that it is actually a queue plus a lock (light weight), the code itself is not complex, but involves a lot of details, it is not easy to read, interested students can go to see, my advice is, from the above summary of the two function points, one is the ring buffer, used to save data; One is the goroutine queue that holds the channel's operations (read and write).

    • BUF is a general-purpose pointer for storing data that focuses on reading and writing to this variable when you look at the source.
    • RECVQ is a read operation that blocks the Goroutine list in the channel, SENDQ is a write operation that blocks the Goroutine list in the channel. The implementation of the list is Sudog, in fact, is a structure of the G package, see the source when the focus on how to block and wake up the goroutine by these two variables

Because of the many sources involved, this is no longer in depth.

Select

Then the select mechanism, Golang's select mechanism can be understood to be implemented at the language level with Select, poll, epoll similar functions: Listen to multiple descriptors of read/write events, once a descriptor is ready (usually read or write events occur), The event can be notified to the concerned application to handle the event. Golang's select mechanism is to listen to multiple channel, each case is an event, can be a read event or write an event, randomly select an execution, you can set the default, its role is: when listening to multiple events are blocking the logic to execute the default.

Select source code in Runtime/select.go, see when the suggestion is to focus on Pollorder and Lockorder

    • Pollorder is the sequence number of the scase, and the random order is for the subsequent execution.
    • Lockorder saved all the address of the channel in the case, where the contiguous memory corresponding to the Lockorder is stacked according to the address size. Chan was ordered to go to the heavy, guaranteeing that all channel locks will not be re-locked when locked.

Because I am the source of this part of the research is not very deep, so donuts can, interested to see the source!

Specific to the demo code: consumer for the specific code, there is only one constantly polling the channel variable stop loop, so the main process is to notify the child when the sub-coprocessor is terminated by stop, in the main method, after close off stop, Reading the closed channel immediately returns a value of 0 for the channel data type, so the <-stop operation in the sub-goroutine returns immediately and exits the run.

In fact, the method of controlling sub-goroutine by channel can be summed up as follows: The loop listens to a channel, generally it is a for loop to put a select monitor channel to achieve the effect of the notification sub-goroutine. With the help of Waitgroup, the master process can wait for all the threads to exit gracefully before ending their own operation, which enables graceful control of the goroutine concurrency beginning and end through the channel.

Channel communication control based on CSP model, compared to the traditional threading and locking concurrency model, avoids a lot of locking unlock performance consumption, and more flexible than the actor model, when using the Actor model, the responsible communication media and execution unit is tightly coupled-each actor has a mailbox. With the CSP model, the channel is the first object that can be created independently, written and read out, and expanded more easily.

Kill device Context

Context is often translated into contexts, which is a relatively abstract concept. The context is also often mentioned when discussing chained invocation techniques. Generally understood as a program unit of a running state, scene, snapshot, and translation in the top and bottom of a good interpretation of its nature, up and down is the existence of the upper and lower layers of transmission, the content will be passed to the next. In the Go language, the program unit is also referred to as Goroutine.

Before executing, each goroutine knows the current execution state of the program, usually encapsulating the execution state in a context variable and passing it to the goroutine to be executed. The context is almost the standard method of passing and requesting the same life cycle variables. Under Network programming, when receiving a request for network requests, in the Goroutine processing this request, it may be necessary to continue to open several new goroutine in the current gorutine to obtain data and logic processing (such as access to the database, RPC service, etc.). That is, a request requires multiple goroutine processing. These goroutine may need to share some information about the request, and all goroutine created from this request should be ended when request is canceled or timed out.

The context is introduced to the standard library after go1.7, and the go version before 1.7 uses the context to install the Golang.org/x/net/context package, for a more detailed description of Golang context, refer to the official documentation: Context

Context Preliminary

The creation and invocation of context is a layer of progressive, that is what we usually call the chain calls, similar data structures in the tree, starting from the root node, each call to derive a leaf node. First, the root node is generated, using the context. The background method is generated, and then chained calls are made using the various methods in the context package, all the methods in the context package:

    • Func Background () Context
    • Func TODO () Context
    • Func Withcancel (parent Context) (CTX context, cancel Cancelfunc)
    • Func Withdeadline (parent Context, Deadline time. Time) (Context, Cancelfunc)
    • Func withtimeout (parent Context, timeout time. Duration) (Context, Cancelfunc)
    • Func Withvalue (parent context, key, Val interface{}) Context

In this case, control concurrency and communication are implemented using only the Withcancel and Withvalue methods:
Words do not say much, on the code:

Package Mainimport ("context" "Crypto/md5" "FMT" "Io/ioutil" "net/http" "Sync" "Time") type Favcontex TKey Stringfunc Main () {wg: = &sync. waitgroup{} values: = []string{"https://www.baidu.com/", "https://www.zhihu.com/"} ctx, Cancel: = context. Withcancel (context. Background ()) for _, URL: = range values {WG. ADD (1) Subctx: = Context. Withvalue (CTX, favcontextkey ("url"), URL) go requrl (SUBCTX, WG)} go func () {time. Sleep (time. Second * 3) Cancel ()} () WG. Wait () fmt. PRINTLN ("Exit main goroutine")}func Requrl (CTX context. Context, WG *sync. Waitgroup) {defer WG. Done () URL, _: = CTx. Value (favcontextkey ("url")). (string) for {select {case <-ctx. Done (): FMT. Printf ("Stop getting url:%s\n", url) return default:r, err: = http. Get (URL) if R.statuscode = = http. Statusok && Err = = Nil {body, _: = Ioutil. ReadAll (r.body) Subctx: = Context. Withvalue (CTX, Favcontextkey ("resp"), FMT. Sprintf ("%s%x", URL, MD5. Sum (body)) WG. ADD (1) Go Showresp (SUBCTX, WG)} r.body.close ()//boot sub goroutine is to not block the current Goro Utine, here in the actual scene can go to perform other logic, here in order to facilitate direct sleep a second//dosometing () time. Sleep (time. Second * 1)}}}func Showresp (CTX context. Context, WG *sync. Waitgroup) {defer WG. Done () for {select {case <-ctx. Done (): FMT. Println ("Stop showing resp") return to Default://Sub goroutine typically handle some IO tasks, such as read-write database or RPC call, here to facilitate direct Data printing FMT. Println ("Printing", ctx. Value (Favcontextkey ("resp"))) time. Sleep (time. Second * 1)}}}

As we said before, the context is designed to solve a scenario where multiple goroutine handle a request and the multiple goroutine need to share some information about the request, which is a simple demo that simulates the process described above.

The context is called first. Background () generates the root node, then calls the Withcancel method, passes in the root node, gets the new child context and the Cancel method of the root node (notifies all child nodes to end the run), here to note: The method also returns a context, This is a new child node, and the initial incoming root node is not the same instance, but each child node will be saved from the original root node to this node link information, in order to implement the chain.

The Requrl method of the program receives a URL, then requests the URL via HTTP to get response, and then in the current goroutine to start a child groutine response print out, Then starting from Requrl the context tree derives the leaf nodes (each chained call to the newly generated CTX), in which each CTX can pass the value in the Withvalue mode (for communication), And each sub-goroutine can be obtained from the parent Goroutine through the value method to achieve the communication between the coprocessor, each child ctx can call the done method to detect whether a parent node calls the Cancel method to notify the child node exit run, The cancel invocation of the root node is communicated to each child node along the link, thus enabling strong concurrency control and the process


Context Call link

The demo combined with the waitgroup to achieve elegant concurrency control and communication, about the principle of waitgroup and the use of the previous article has been done, here will not repeat, of course, the actual application scenario will not be so simple, The goroutine that processes the request starts multiple sub-goroutine mostly processing IO-intensive tasks such as read-write databases or RPC calls, and then continues to execute other logic in the main goroutine, where the simplest processing is done for ease of interpretation.

Context as a golang in the concurrency control and communication of the big Kill, is widely used, some people use go to develop HTTP services, if you read these many web framework source code will know that the context in the web framework everywhere, Because HTTP request processing is a typical chained process and concurrency scenario, many web frameworks use the context to implement chained invocation logic. Interested can read the context package source code, you will find that the implementation of the context is actually a combination of mutex lock and channel to achieve, in fact, many concurrent, synchronous high-level components original aim, are through the most basic data structure assembled, as long as the knowledge of the most fundamental concept, The upstream architecture can also be at a glance.

Context Usage Specification

Finally, although the context is an artifact, developers use it to follow the Basic Law, the following are some of the criteria used in the context:

    • Do not store contexts inside a struct type; Instead, pass a Context explicitly to each function that needs it. The context should be the first parameter, typically named CTX; do not put the context into a struct, explicitly passing in the function. The context variable needs to be used as the first parameter, generally named CTX;

    • Do not pass a nil Context, even if a function permits it. Pass context. TODO If you is unsure about which context to use, even if the method allows, do not pass in a nil context, if you're not sure what context you want to pass a context.todo;

    • Use context Values request-scoped data transits processes and APIs, not for passing optional parameters to F unctions; The value-related method using the context should only be used for metadata that is passed in the program and interface and requested, not to pass some optional arguments;

    • The same Context is passed to functions running in different goroutines; Contexts is safe for simultaneous using by multiple goroutines; The same context can be used to transfer to different goroutine, the context is safe in multiple goroutine;

Reference links

    • [1] https://deepzz.com/post/golang-context-package-notes.html
    • [2] http://www.flysnow.org/2017/05 /12/go-in-action-go-context.html
    • [3] https://golang.org/pkg/context/
    • [4]http://www.moye.me/2017 /05/05/go-concurrency-patterns/

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.