Golang Common Concurrent Programming Tips was first published on blog.hdls.me/15726777274…
Golang was one of the first languages to incorporate CSP principles into its core and introduce this style of concurrent programming to the masses. CSP refers to Communicating Sequential Processes, in which each instruction needs to specify whether it is an output variable (in the case of reading a variable from a process) or a destination (in the case of sending input to a process).
Golang not only provides cSP-style concurrency, but also supports the traditional way of synchronizing through memory access. This article summarizes the most commonly used Golang concurrency programming tools.
Sync package
The Sync package contains the most useful concurrency primitives for low-level memory access synchronization, is the most powerful tool for “memory access synchronization” and is a common tool for solving critical sections in traditional concurrency models.
WaitGroup
WaitGroup is a method that waits for a set of concurrent operations to complete and contains three functions:
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done(a)
func (wg *WaitGroup) Wait(a)
Copy the code
Add() is used to Add the number of goroutines, Done() is used to indicate that the execution is complete and exits, reducing the count by one, and Wait() is used to Wait for all goroutines to exit.
Usage:
func main(a) {
wg := sync.WaitGroup{}
wg.Add(1)
go func(a) {
defer wg.Done()
fmt.Printf("End of goroutine \ n")
}()
wg.Wait()
}
Copy the code
Note that the Add() method needs to be executed before Goroutine.
Mutex and read-write locks
Mutual exclusion is a way to protect critical sections in a program. A mutex can only be locked by one Goroutine at a time, and the other Goroutines will block until the mutex is unlocked (relocking the mutex).
Usage:
func main(a) {
var lock sync.Mutex
var count int
var wg sync.WaitGroup
wg.Add(1)
/ / count + 1
go func(a) {
defer wg.Done()
lock.Lock()
defer lock.Unlock()
count++
fmt.Println("count=", count)
}()
/ / count minus 1
wg.Add(1)
go func(a) {
defer wg.Done()
lock.Lock()
defer lock.Unlock()
count--
fmt.Println("count=", count)
}()
wg.Wait()
fmt.Println("count=", count)
}
Copy the code
Note that it is a common idiom to invoke Unlock with defer in Goroutine, ensuring that even if a panic occurs, the call is always executed to prevent deadlocks.
Read/write locks are conceptually the same as mutex locks: they protect access to memory, and read/write locks give you more control internally. The biggest difference between a read/write lock and a mutex lock is that the read/write lock can be locked separately. Generally, it is used for a large number of read operations and a small number of write operations.
Lock() and Unlock() for read/write locks Lock and Unlock for write operations. Rlock() and RUnlock() lock and unlock read operations and need to be paired. Read locks and write locks
- Only one Goroutine can get a write lock at a time.
- Any number of Gorouintes can acquire read locks at the same time.
- Only write or read locks can exist simultaneously (read and write are mutually exclusive).
Channel
Channel, one of the synchronization primitives derived from CSP, is the most advantageous tool for Golang’s idea of “using communication to share memory, rather than communicating through shared memory”.
The basic use of Channel is not explained here, but the results of different operations of Channel in different states are summarized:
operation | The Channel state | The results of |
---|---|---|
Read | nil | blocking |
Open is not empty | The output value | |
Open but empty | blocking | |
Shut down | < default value >, false | |
Just write | Compile error | |
Write | nil | blocking |
Open but fill | blocking | |
Open the discontent | Write the value | |
Shut down | panic | |
read-only | Compile error | |
Close | nil | panic |
Open is not empty | Close the Channel; The read succeeds until the Channel runs out, reading the default value of the generated value | |
Open but empty | Close the Channel; Read the default values for producers | |
Shut down | panic | |
read-only | Compile error |
for-select
The SELECT statement is the glue that binds channels together, enabling a Goroutine to wait for multiple channels to reach a ready state.
A SELECT statement is an operation on a Channel that looks syntactically similar to a switch, except that the case statements in the SELECT block have no test order and do not fail if none of the conditions are met. Usage:
var c1, c2 <-chan interface{}
select {
case <- c2:
// some logic
case <- c2:
// some logic
}
Copy the code
The select control structure above waits for the return of any one of the case condition statements, which executes the code in the case immediately, but if both cases in the SELECT are fired at the same time, a random case is selected for execution.
For-select is a very common usage, usually used in the case of “sending an iteration variable to a Channel” and “loop waiting to stop”. It is used as follows:
Send the iteration variable to a Channel:
func main(a) {
c := make(chan int.3)
for _, s := range []int{1.2.3} {
select {
case c <- s:
}
}
}
Copy the code
Loop wait stop:
/ / the first
for {
select {
case <- done:
return
default:
// Perform non-preemptive tasks}}/ / the second
for {
select {
case <- done:
return
default:}// Perform non-preemptive tasks
}
Copy the code
The first is that when we enter the SELECT statement, if the completed Channel is not closed, we execute the default statement. The second means that if the completed Channel is not closed, we exit the SELECT statement and continue executing the rest of the for loop.
done channel
Although Goroutines are cheap and easy to use, and the runtime can reuse multiple Goroutines to any number of operating system threads, it is important to know that Goroutines consume resources and are not garbage collected by the runtime. If goroutine leaks occur, they can cause memory utilization to decrease in severe cases.
Done Channel is a great way to prevent Goroutine leaks. Establish a “signal channel” between parent and child Goroutines using the Done channel. The parent goroutine can pass this channel to the child goroutine and then close it when it wants to cancel the child Goroutine. Usage:
func main(a) {
doneChan := make(chan interface{})
go func(done <-chan interface{}) {
for {
select {
case <-done:
return
default:
}
}
}(doneChan)
// Parent goroutine closes child Goroutine
close(doneChan)
}
Copy the code
One way to ensure that a Goroutine does not leak is to have a convention: if a Goroutine is responsible for creating a Goroutine, it is also responsible for making sure that it can stop a Goroutine.
The Context package
The Context package is designed to simplify operations related to data, cancellation signals, deadlines, and so on in the request domain between multiple Goroutines handling a single request, which may involve multiple API calls. The purpose of the Context package is twofold: to provide an API that can cancel branches in your call diagram, and to provide packets that request scope data through call transfer.
If you use the Context package, then every function downstream of the top-level concurrent invocation takes Context as its first argument.
Context has the following types:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}}Copy the code
The Deadline function is used to indicate whether the goroutine will be cancelled after a certain time. The Done method returns the Channel that was closed when our function was preempted; The Err method returns an error cause for cancellation, because what Context was canceled; The Value function returns the key or nil associated with this Context.
Context is an interface, but we do not need to implement it when we use it. The Context package has two built-in methods to create instances of the Context:
func Background(a) Context
func TODO(a) Context
Copy the code
Background is used in the main function, initialization, and test code. Context is the top-level Context in the tree and cannot be cancelled. TODO, we can use this if we don’t know what Context to use, but in practice, we haven’t used this TODO yet.
And then we’re going to use that as the parent Context at the top level, spawn off the child Context and start the call chain. And these Context objects form a tree, and when the parent Context object is canceled, all of its child contexts are canceled. The context package also provides a set of functions to generate child contexts:
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
Copy the code
Where WithCancel returns a new Context and closes its done channel when the returned Cancel function is called; WithDeadline returns a new Context that closes the completed channel when the machine’s clock passes a given deadline; WithTimeout returns a new Context and closes its completed channel after the given timeout; WithValue generates a Context bound to a key-value pair of data that can be accessed using the context. Value method.
Here’s how to use it:
WithCancel
func main(a) {
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go func(ctx context.Context) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("Err:", ctx.Err())
return
default:
}
}
}(ctx)
cancel()
wg.Wait()
}
Copy the code
WithDeadline
func main(a) {
d := time.Now().Add(1 * time.Second)
wg := sync.WaitGroup{}
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
wg.Add(1)
go func(ctx context.Context) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("Err:", ctx.Err())
return
default:
}
}
}(ctx)
wg.Wait()
}
Copy the code
WithTimeout
func main(a) {
wg := sync.WaitGroup{}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
wg.Add(1)
go func(ctx context.Context) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("Err:", ctx.Err())
return
default:
}
}
}(ctx)
wg.Wait()
}
Copy the code
WithValue
func main(a) {
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
valueCtx := context.WithValue(ctx, "key"."add value")
wg.Add(1)
go func(ctx context.Context) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("Err:", ctx.Err())
return
default:
fmt.Println(ctx.Value("key"))
time.Sleep(1 * time.Second)
}
}
}(valueCtx)
time.Sleep(5*time.Second)
cancel()
wg.Wait()
}
Copy the code
Reference: Concurrency in Go