Control concurrency There are two classic ways, one is Waitgroup, the other is the context, today I will talk about the context.
What is Waitgroup
Waitgroup before we introduced in the concurrency, it is a way to control concurrency, it is the way to control multiple goroutine at the same time to complete.
func main() { var wg sync.WaitGroup wg.Add(2) go func() { time.Sleep(2*time.Second) fmt.Println("1号完成") wg.Done() }() go func() { time.Sleep(2*time.Second) fmt.Println("2号完成") wg.Done() }() wg.Wait() fmt.Println("好了,大家都干完了,放工")}
A very simple example, must be an example of the 2 goroutine at the same time, it is finished, the first to do is to wait for the other unfinished, all the goroutine to be complete.
This is a way of controlling concurrency, which is especially true when a lot of goroutine work together to do one thing, because each goroutine is part of this thing, and only the whole goroutine is done, and this is the way to wait.
In the actual business species, we may have a scenario where we need to proactively inform the end of a goroutine. For example, we open a backstage goroutine always do things, such as monitoring, now do not need, you need to inform this monitoring goroutine end, otherwise it will keep running, the leak.
Chan Notice
We all know a goroutine boot, we are unable to control his, most of the situation is to wait for its own end, then if this goroutine is not the end of their own backstage goroutine it? such as monitoring and so on, will continue to run.
This situation, has been a fool-like approach is a global variable, other places by modifying this variable to complete the end of the notification, and then the background goroutine constantly check the variable, if the notice is closed, the self-end.
This is also possible, but first we have to make sure that this variable is safe under multithreading, and there is a better way to do this: Chan + Select.
func main() { stop := make(chan bool) go func() { for { select { case <-stop: fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }() time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") stop<- true //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second)}
In the example we define a stop
Chan that informs him to end the backstage goroutine. The implementation is also very simple, in the background goroutine, use Select to determine stop
whether the value can be received, if you can receive, you can exit the stop, if not received, will execute default
the monitoring logic, continue to monitor, only to receive stop
notifications.
With the above logic, we can send a value to Chan in other goroutine, the stop
example being sent in the main goroutine, the control to let the goroutine end of this monitoring.
After sending the stop<- true
end of the instruction, I use a time.Sleep(5 * time.Second)
deliberate pause for 5 seconds to detect if we have finished monitoring the goroutine successfully. If it succeeds, no more output will be available, goroutine监控中...
and if not, monitoring goroutine will continue to print goroutine监控中...
out.
This chan+select way, is more elegant end a goroutine way, but this method also has limitations, if there are many goroutine need to control the end of what to do? What if these goroutine have spawned more goroutine? What if the endless goroutine of a layer? This is very complicated, and even if we define many Chan it is difficult to solve this problem because the goroutine relationship chain leads to the complexity of this scenario.
First knowledge of the context
The scenario mentioned above is there, such as a request for network requests, each request needs to open a goroutine to do something, and these goroutine may open other goroutine. So we need a way to keep track of goroutine, so we can control their purpose, which is the context that the go language provides for us, which is the context of the goroutine.
Let's use go context to rewrite the example above.
func main() { ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second)}
Rewriting is relatively simple, is to change the original Chan to the stop
context, using the context tracking goroutine, in order to control, such as the end.
context.Background()
Returns an empty context, which is typically used for the root node of the entire context tree. We then use the context.WithCancel(parent)
function to create a sub-context that can be canceled and then pass it as a parameter to Goroutine, so that we can use this sub-context to trace the goroutine.
In Goroutine, the Select call <-ctx.Done()
is used to determine if the end is to be completed, and if the value is accepted, the end goroutine can be returned, and if it is not received, it will continue to be monitored.
So how do you send the end order? This is the function in the example cancel
, which is returned when we call the context.WithCancel(parent)
function to generate a sub-context, and the second return value is the cancellation function, which is the CancelFunc
type. We call it and we can issue a cancellation command, and then our monitoring goroutine will receive a signal and it will return to the end.
Context Controls multiple Goroutine
Using context to control a goroutine example, as above, very simple, below we look at the control of multiple goroutine examples, in fact, it is relatively simple.
func main() { ctx, cancel := context.WithCancel(context.Background()) go watch(ctx,"【监控1】") go watch(ctx,"【监控2】") go watch(ctx,"【监控3】") time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second)}func watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Println(name,"监控退出,停止了...") return default: fmt.Println(name,"goroutine监控中...") time.Sleep(2 * time.Second) } }}
The example launches 3 monitoring goroutine for continuous monitoring, each using the context to track, and when we use cancel
function notification cancellation, these 3 goroutine will be ended. This is the control of the context, it is like a controller, the switch is pressed, all based on this context or derived sub-context will be notified, then the cleanup operation, finally released Goroutine, This elegantly solves the problem that the goroutine is not controllable after startup.
Context interface
The context of the interface definition is relatively concise, we 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{}}
This interface has a total of 4 methods, it is important to understand the meaning of these methods, so that we can better use them.
Deadline
The method is to get the cutoff time of the set, the first return is the deadline, at which point the context will automatically initiate the cancellation request, the second return value Ok==false indicates that no deadline is set, and if cancellation is required, call the Cancel function to cancel.
Done
method returns a read-only Chan with the type struct{}
goroutine, if the method returned by Chan can read, it means that the parent context has initiated a cancellation request, and Done
after we receive this signal through the method, we should do a cleanup operation, Then exit Goroutine and release the resource.
Err
method returns the cause of the cancellation error because what context was canceled.
Value
Method gets the value bound by the context, which is a key-value pair, so a key can be used to obtain the corresponding value, which is generally thread-safe.
The above four methods commonly used is Done
, if the context is canceled, we can get a closed Chan, the closed Chan is readable, so as long as you can read, it means to receive the context cancellation signal, the following 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 require us to implement, the go built-in has helped us achieve 2, our code in the beginning is the two built-in as the top-level partent context, derived more sub-context.
var ( background = new(emptyCtx) todo = new(emptyCtx))func Background() Context { return background}func TODO() Context { return todo}
One is Background
, mainly used in the main function, initialization and test code, as the context of the top-level context of the tree structure, that is, the root context.
One is TODO
that it does not yet know the specific usage scenario and can use this if we do not know what context to use.
Both of them are essentially emptyCtx
struct types, a context that cannot be canceled, has no set deadlines, and does not carry any values.
type emptyCtx intfunc (*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 is the emptyCtx
way to implement the context interface, and you can see that these methods do nothing and return nil or 0 values.
Derivation of context Inheritance
With the root context, how do you derive more sub-context? This will depend on the context package for us to provide 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 four With
functions, receive a partent parameter, is the parent context, we want to create a sub-context based on this parent context, this way can be understood as the child context of the parent context inheritance, It can also be understood as a derivation based on the parent context.
With these functions, a context tree is created, each node of the tree can have any number of child nodes, and the node hierarchy can have any number of nodes.
WithCancel
The function, passing a parent context as a parameter, returns a child context, and a cancellation function is used to cancel the context. WithDeadline
function, and WithCancel
almost, it will pass a cutoff time parameter, meaning that at this point in time, will automatically cancel the context, of course, we can not wait until this time, can be canceled in advance by canceling the function.
WithTimeout
And WithDeadline
Basically, this means that the timeout is automatically canceled, and the meaning of the context is automatically canceled after the time.
WithValue
The function is not related to canceling the context, it is to generate a context that binds a key-value pair of data, which can be accessed by means of a Context.Value
method, which we will specifically talk about later.
You may notice that the first three functions return a cancellation function CancelFunc
, which is a function type, and its definition is very simple.
type CancelFunc func()
This is the type of cancellation function that cancels a context and all the context of the node under context, regardless of the number of levels.
Withvalue passing meta data
Through the context we can also pass some necessary metadata that is appended to the context for use.
var key string="name"func main() { ctx, cancel := context.WithCancel(context.Background()) //附加值 valueCtx:=context.WithValue(ctx,key,"【监控1】") go watch(valueCtx) time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second)}func watch(ctx context.Context) { for { select { case <-ctx.Done(): //取出值 fmt.Println(ctx.Value(key),"监控退出,停止了...") return default: //取出值 fmt.Println(ctx.Value(key),"goroutine监控中...") time.Sleep(2 * time.Second) } }}
In the previous example, we passed the values to the monitoring function by passing the parameters name
. In this example, we achieve the same effect, but through the context of the value of the way.
We can use the context.WithValue
method to attach a pair of k-v key-value pairs, where key must be equivalent, that is, comparable; Value value is thread-safe.
This gives us a new context, a new context with this key-value pair, which can be read by method when used Value
ctx.Value(key)
.
Keep in mind that using Withvalue to pass a value is generally a required value, not what value is passed.
Principles of use of the Context
- Do not place the context in a struct, pass it as a parameter
- Using context as a function method of parameters, the context should be the first parameter, put in the first place.
- When passing a context to a function method, do not pass nil and use context if you do not know what to pass. Todo
- The context's value-related method should pass the necessary data, and no data is used for this pass
- The context is safe for the county, can be assured in the delivery of multiple Goroutine