Go language - Go Context

There are two classic ways to control concurrency. One is WaitGroup and the other is Context. Today I will talk about Context.

What is WaitGroup

WaitGroup was introduced in the past when we were concurrent. It is a way to control concurrency. It is controlled by multiple gorouts at the same time.

Func main() {
Var wg sync.WaitGroup

wg.Add(2)
Go func() {
time.Sleep(2*time.Second)
fmt.Println("Complete 1")
wg.Done()
}()
Go func() {
time.Sleep(2*time.Second)
fmt.Println("Completion of No. 2")
wg.Done()
}()
wg.Wait()
fmt.Println("Okay, everyone is done, let go")
}

A very simple example, the two goroutines in the example must be completed at the same time, it is completed, the first thing to do is to wait for other unfinished, all goroutines must be completed.

This is a way to control concurrency. This is especially useful when multiple goroutines work together to do one thing, because each goroutine is part of this thing, only all goroutines are done, this thing It is completed, this is the way to wait.

In the actual business kind, we may have such a scenario: we need to actively notify a certain goroutine to end. For example, we open a background goroutine has been doing things, such as monitoring, now need not, you need to inform the monitoring goroutine end, otherwise it will run all the time, it will leak.

Chan notice

We all know that after a goroutine starts, we can't control him. Most of the time it is waiting for it to end itself. So if this goroutine is a background goroutine that won't end itself? For example, monitoring, etc., will always run.

In this case, the fool-like approach is a global variable. Others modify the variable to complete the end notification, and then the background goroutine keeps checking the variable. If it is found to be closed, it ends itself.

This is fine, but first we have to ensure that this variable is safe under multithreading. Based on this, there is a better way: chan + select.

Func main() {
Stop := make(chan bool)

Go func() {
For {
Select {
Case <-stop:
fmt.Println("Monitor exit, stopped...")
Return
Default:
fmt.Println("goroutine monitoring...")
time.Sleep(2 * time.Second)
}
}
}()

time.Sleep(10 * time.Second)
fmt.Println("Yes, notification monitoring stopped")
Stop<- true
/ / In order to detect whether the monitoring has stopped, if there is no monitoring output, it means stop
time.Sleep(5 * time.Second)

}

In the example we define aThe chan of stop informs him to end the background goroutine. The implementation is also very simple, in the background goroutine, use select to determineStopIs it possible to receive a value, if it can be received, it means that it can be stopped and stopped; if it is not received, it will be executed.Monitoring logic in default, continue to monitor, only receivedNotification of stop.

With the above logic, we can give other goroutine speciesStopchan sends the value, the example is sent in the main goroutine, the control ends the gorout of this monitor.

SentStop<- true After the end of the instruction, I use it heretime.Sleep(5 * time.Second)Deliberately pause for 5 seconds to detect if we ended up monitoring the goroutine successfully. If successful, there will be no moreGoroutine monitors the output of ; if it is not successful, the monitor goroutine will continue to printGoroutine monitoring...Output.

This chan+select method is a more elegant way to end a goroutine, but this method also has limitations. If there are many goroutines that need control to end, what should I do? What if these goroutines have spawned more goroutines? What if there is an endless goroutine? This is very complicated, even if we define a lot of chan, it is very difficult to solve this problem, because the goroutine relationship chain leads to this scene is very complicated.

First Context

The above scenario is there, such as a network request Request, each Request needs to open a goroutine to do something, these goroutine may open other goroutine. So we need a way to track goroutine, in order to achieve their purpose of control, this is the Context provided by the Go language, called the context is very appropriate, it is the context of the goroutine.

Let's rewrite the above example using Go Context.

Func main() {
Ctx, cancel := context.WithCancel(context.Background())
Go func(ctx context.Context) {
For {
Select {
Case <-ctx.Done():
fmt.Println("Monitor exit, stopped...")
Return
Default:
fmt.Println("goroutine monitoring...")
time.Sleep(2 * time.Second)
}
}
}(ctx)

time.Sleep(10 * time.Second)
fmt.Println("Yes, notification monitoring stopped")
Cancel()
/ / In order to detect whether the monitoring has stopped, if there is no monitoring output, it means stop
time.Sleep(5 * time.Second)

}

Rewriting is relatively simple, is to turn the original chanStopChange to Context, use Context to track goroutine for control, such as end.

context.Background() returns an empty Context, which is typically used for the root node of the entire Context tree. Then we useThe context.WithCancel(parent) function creates a cancelable child Context and passes it as a parameter to the goroutine so that it can be used to track the goroutine.

In the goroutine, use the select call<-ctx.Done()To determine if you want to end, if you receive the value, you can return to the end goroutine; if you can't receive it, it will continue to monitor.

So how do you send the end command? This is the exampleCancel function, it is our callThe context.WithCancel(parent) function returns when the child Context is generated. The second return value is the cancel function, which isCancelFunc type. We can call it to issue a cancel command, and then our monitor goroutine will receive a signal and it will return to the end.

Context controls multiple goroutines

The example of using Context to control a goroutine is as simple as above. Let's take a look at an example of controlling multiple goroutines, which is actually quite simple.

Func main() {
Ctx, cancel := context.WithCancel(context.Background())
Go watch(ctx,"[Monitor 1]")
Go watch(ctx,"[Monitor 2]")
Go watch(ctx,"[Monitor 3]")

time.Sleep(10 * time.Second)
fmt.Println("Yes, notification monitoring stopped")
Cancel()
/ / In order to detect whether the monitoring has stopped, if there is no monitoring output, it means stop
time.Sleep(5 * time.Second)
}

Func watch(ctx context.Context, name string) {
For {
Select {
Case <-ctx.Done():
fmt.Println(name, "Monitor exit, stopped...")
Return
Default:
fmt.Println(name,"goroutine monitoring...")
time.Sleep(2 * time.Second)
}
}
}

In the example, three monitoring goroutines are started for continuous monitoring, each using Context for tracking, when we useWhen the cancel function notification is canceled, these 3 gorouts will be ended. This is the control ability of the Context. It is like a controller. After pressing the switch, all the sub-Contexts based on this Context or derived will receive a notification. Then you can clean up and finally release the goroutine, which is elegant. Solved the problem that the goroutine is uncontrollable after startup.

"Go language combat" study notes, to be continued, welcome to scan the code to pay attention to the public numberFlysnow_org or the website http://www.flysnow.org/, see the follow-up notes in the first place. If you feel helpful, share it with your friends and thank you for your support.

Context interface

The interface definition of Context is relatively simple, let's look at the method of this interface.

type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key interface{}) interface{}
}

There are four methods for this interface. It is important to understand the meaning of these methods so that we can use them better.

The Deadline method is to get the set deadline. The first return is the cutoff time. At this point, the Context will automatically initiate the cancel request. The second return value ok==false means that the deadline is not set. If you need to cancel, you need to call the cancel function to cancel.

The Done method returns a read-only chan of typeStruct{}, we are in the goroutine, if the chan returned by the method can be read, it means that the parent context has initiated the cancellation request, we passAfter the Done method receives this signal, it should do the cleanup operation, then exit the goroutine and release the resources.

The Err method returns the reason for the cancellation because the Context was canceled.

The Value method gets the value bound on the Context, which is a key-value pair, so you need to get the corresponding value through a Key. This value is generally thread-safe.

Commonly used in the above four methods isDone, if the Context is canceled, we can get a closed chan, the closed chan is readable, so as long as it can be read, it means that the Context cancel signal is received, below Is the classic usage of this method.

  func Stream(ctx context.Context, out chan<- Value) error {
  	for {
  		v, err := DoSomething(ctx)
  		if err != nil {
  			return err
  		}
  		select {
  		case <-ctx.Done():
  			return ctx.Err()
  		case out <- v:
  		}
  	}
  }

The Context interface does not need to be implemented by us. The Go built-in has already implemented 2 of them. The first part of our code is to use the two built-in top-level partent contexts to spawn more sub-Contexts.

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

one isBackground, mainly used in the main function, initialization, and test code, as the top-level Context of the Context tree structure, which is the root Context.

one isTODO, it doesn't know the specific usage scenario yet. If we don't know what Context to use, we can use this.

Both of them are essentiallyThe emptyCtxstructure type is a non-cancelable, no set deadline, no Context carrying any value.

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done() <-chan struct{} {
	return nil
}

func (*emptyCtx) Err() error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

This isemptyCtxImplementation of the Context interface method, you can see that these methods do nothing, returning nil or zero value.

Context inheritance derivative

With the root Context above, how do you derive more sub-Contexts? This depends on the context package for us.The With series of functions.

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

These fourWith function, the received one has a partent parameter, which is the parent Context. We want to create a sub-Context based on this parent Context. This way can be understood as the inheritance of the child Context to the parent Context. It can also be understood as based on Derivation of the parent Context.

Through these functions, a Context tree is created. Each node of the tree can have any number of child nodes, and the node level can have any number of nodes.

The WithCancel function takes a parent Context as a parameter, returns a child Context, and a cancel function to cancel the Context.WithDeadlinefunction, andWithCancelAlmost, it will pass a deadline parameter, which means that at this point in time, the Context will be automatically canceled. Of course, we can wait until this time, and cancel it by canceling the function in advance.

WithTimeout andWithDeadline basically the same, this means that the timeout is automatically canceled, and it is the time to automatically cancel the Context after the time.

The WithValue function has nothing to do with canceling the Context. It is to generate a Context that binds a key-value pair data. The bound data can pass.The Context.Value method is accessed, we will talk about it later.

You may notice that the first three functions return a cancel function.CancelFunc, which is a function type, its definition is very simple.

type CancelFunc func()

This is the type of the cancel function, which cancels a Context and all the Contexts under the Context of the node, no matter how many levels.

WithValue to pass metadata

Through the Context we can also pass some necessary metadata, which will be attached to the Context for use.

Var key string="name"

Func main() {
Ctx, cancel := context.WithCancel(context.Background())
//Additional value
valueCtx:=context.WithValue(ctx,key,"[Monitor 1]")
Go watch(valueCtx)
time.Sleep(10 * time.Second)
fmt.Println("Yes, notification monitoring stopped")
Cancel()
/ / In order to detect whether the monitoring has stopped, if there is no monitoring output, it means stop
time.Sleep(5 * time.Second)
}

Func watch(ctx context.Context) {
For {
Select {
Case <-ctx.Done():
/ / Take the value
fmt.Println(ctx.Value(key),"Monitor exit, stopped...")
Return
Default:
/ / Take the value
fmt.Println(ctx.Value(key),"goroutine monitoring...")
time.Sleep(2 * time.Second)
}
}
}

In the previous example, we passed the parameters in the wayThe value of name ​​is passed to the monitor function. In this example, we achieve the same effect, but through the way of the Value of the Context.

We can useThe context.WithValue method appends a pair of K-V key-value pairs, where the Key must be equivalence, that is, comparable; the Value value is thread-safe.

So we generate a new Context, this new Context with this key-value pair, when used, can passValue method readctx.Value(key)

Remember, passing values ​​with WithValue is generally a required value, and no values ​​are passed.

Context usage principle

  1. Don't put the Context in the structure, you need to pass it as a parameter
  2. The function method with Context as the parameter should put the Context as the first parameter and put it first.
  3. When passing a Context to a function method, don't pass nil. If you don't know what to pass, use context.TODO
  4. Context's Value related method should pass the necessary data, do not use any data to pass this pass
  5. Context is thread-safe and can be safely passed in multiple goroutines