Golang’s most well-known concurrency model is the CSP concurrency model, also known as the GMP concurrency model composed of Goroutine and Channel. Here, I’ll talk about Golang’s other ways of concurrency.

Golang can use not only CSP concurrent mode, but also the traditional concurrent mode of sharing data.

Critical section

This is the more common way in traditional languages, that is, locking. The lock synchronizes its threads, allowing only one Goroutine at a time to enter a code block called the critical section.

Golang provides packages of mutex and conditional variables for critical sections.

The mutex

It’s a common lock used to serialize threads. Golang provides the Mutex sync.Mutex and the read-write Mutex sync.RWMutex, which is extremely simple to use:

var s sync.Mutex
    
s.Lock()
    
// The code here is serial, ho ho ho...
    
s.Unlock()


Copy the code

The Lock and Unlock

sync.Mutexandsync.RWMutexThe difference between

It’s not much different, but sync.rwmutex is more nuanced and treats “read” and “write” operations differently. Lock and unLock in sync.RWMutex are for write operations

var s sync.RWMutex

s.Lock()

// It says lock, ho ho

s.Unlock()
Copy the code

RLock and RUnLock in sync.rwmutex are for read operations

var s sync.RWMutex

s.RLock()

// Read the lock, ho ho..

s.RUnlock()
Copy the code

Read/write locks have the following rules:

  • The write lock is locked, and both the read lock and the write lock block
  • The read lock is locked, the write lock is blocked, the read lock is not blocked

That is, multiple write operations and read operations cannot be performed at the same time. Multiple read operations can be performed at the same time

Matters needing attention:

  • Do not lock mutex repeatedly; Because code is cumbersome and error-prone, a single deadlock can fail. Panic thrown by the system during the Go language runtime is a fatal error and cannot be recoveredrecoverThe function doesn’t do anything to them. Once a deadlock occurs, the program must crash.
  • Lock and unlock must appear in pairs, if you are afraid of forgetting to unlock, it is best to usedeferStatement to unlock; However, it is important not to unlock unlocked or locked mutex locks because they triggerpanic, and thepanicLike a deadlock, this is a fatal error and the program must crash
  • sync.MutexIs a structure, try not to use it as a parameter, directly propagated in multiple functions. Golang’s parameters are copies, which are independent of each other.

The condition variable Cond

Mutex is used to lock resources and “create” critical sections. The condition variable Cond can be thought of as a self-scheduling thread (in this case, groutine) that blocks and waits when the state changes and wakes up when the state changes.

Sync. Mutex and sync.RWMutex can be used without Mutex. All Cond initializations require a mutex. (ps: even if the initialization is not required, the application scenario requires a mutex.)

Cond supports Wait, Signal, and Broadcast. Wait indicates that a thread (groutine) blocks and waits; Signal indicates the groutine to wake up waiting; Broadcast represents all groutine waiting to wake up;

Initialization:

cond := sync.NewCond(&sync.Mutex{})
Copy the code

In one of the Groutine:

cond.L.Lock()
for status == 0 {
     cond.Wait()
}
// State change, goroutine wake up, what to do...
cond.L.Unlock()
Copy the code

This is a template

In another Groutine:

cond.L.Lock()
status = 1
cond.Signal() // Or use cond.broadcast () to wake up the sleeping Groutine in the above groutine
cond.L.Unlock()
Copy the code

Atomic operation

Atomic operations are supported at the hardware chip level, so absolute thread-safety is guaranteed. And execution is orders of magnitude more efficient than other methods.

Atomic operations in Go are of course CPU and operating system based. The package for atomic operations in Go is sync/atomic, This package provides Add, CAS(swap and compare compare and swap), store and load in pairs, and swap.

Most of the functions provided in this package are also very single data types: only integers! The way to use is very simple, look at the function directly call good.

var a int32
a = 1
a = atomic.AddInt32(&a, 2) // Atomic operation here, simple as that, ho ho
Copy the code

The CAS function prefix is “CompareAndSwap”, which means “CompareAndSwap”. When the CAS operation is performed, the function determines whether the current value of the variable being manipulated is equal to the expected old value. If so, it assigns the new value to the variable and returns true. Otherwise, it ignores the operation and returns false.

Golang may provide a limited number of data types for atomic operations, but Go also adds a structure called atomic.Value, which is equivalent to a small container that can provide store and extract loads for atomic operations

var atomicVal atomic.Value
str := "hello"

atomicVal.Store(str) // This is an atomic operation

newStr := atomicVal.Load() // This is an atomic operation
Copy the code

other

To better schedule goroutine, Go provides sync.waitGroup, sync.once, and context

sync.WaitGroup

The purpose of sync.waitgroup is to make the main Goroutine wait for all goroutine executions to complete in a multi-goroutine concurrent program. Sync.waitgroup provides three functions: Add, Done, and Wait:

  • Add is written to the main Goroutine and takes the number of goroutines to run
  • Done is written in each non-main goroutine to indicate the end of the run
  • Wait is written in the main goroutine, which blocks the main goroutine and waits for all other goroutines to finish running
var wait sync.WaitGroup

    wait.Add(2) // Must be the number of goroutines running

    go func(a) {
        //TODO a small operation
        defer wait.Done() The done function is used in goroutine to indicate the end of the goroutine operation} ()go func(a) {
        //TODO a small operation
        defer wait.Done() The done function is used in goroutine to indicate the end of the goroutine operation
    }()

    wait.Wait() // Block lives until all goroutines are finished

Copy the code

Pay attention to

WaitGroup has a counter that records the number of goroutines that need to wait. The default value is 0. You can Add or decrease the value, but don’t set the counter to less than zero, as panic!

Sync.WaitGroup The counter in sync.WaitGroup must be 0 when the Wait method is called. So the value in Add must equal the number of non-primary Goroutines! And don’t execute Add and Wait methods in different Goroutines!

sync.Once

Really only do it once.

Sync.once takes only one method, Do, and one parameter, func. Just copy the following code and guess the result.

package main

import (
    "fmt"
    "sync"
)

func main(a) {
    var once sync.Once
    onceBody := func(a) {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func(a) {
            once.Do(onceBody)
            done <- true(1)}}for i := 0; i < 10; i++ {
        <-done
    }
}

Copy the code

The execution result

Only once
Copy the code

That’s right, just one line. It was only executed once.

context

Context can be used to implement one-to-many Goroutine collaboration. The application scenario of this package is mainly in the API. The literal meaning is also straightforward, the context. When a request is made, a Goroutine is generated, but this Goroutine often spawns many additional Goroutines to handle operations such as linking databases and requesting RPC requests. These derived Goroutines and the main goroutine have a lot of common data, such as the same request life cycle, user authentication information, token, etc. When the request times out or is cancelled, all goroutines should end. Context will help us do that.

Obviously, a tree structure is formed between the main Goroutine and all of its descendants. Our context can spread across the tree from the root node, and of course, it’s thread-safe.

The basic between threads looks like this:

func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}
Copy the code

There are two roots context:background and todo; Both roots are contenxt empty and have no value. There’s not much difference, Background is the most commonly used Context, the Context that’s at the top of the tree, it can’t be cancelled. You can use TODO when you don’t know what context to use.

The root generates child nodes in the following ways:

// Generate an undoable Context (manually undo)
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)// Generate a timer undoContext (Scheduled cancellation)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)// Also generates periodic undoContext (Scheduled cancellation)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)// It is irrevocableContextYou can save onekvThe value of thefunc WithValue(parent Context, key, val interface{}) Context
Copy the code

Undoable Context

Here’s how each method is called (all from goDoc, paste reusable) : revocable func WithCancel(parent Context) (CTX Context, cancel CancelFunc)

    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func(a) {
            for {
                select {
                case <-ctx.Done(): // This is triggered only after the undo function is called
                    return 
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()  // Call the returned cancel method to have the context declare the end of the cycle

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break}}Copy the code

To terminate all threads, call the cancel function returned by CTX, cancel := context.withcancel (context.background ()). Which is the channel that its Done method returns.

WithDeadline and WithTimeout are basically used similarly, and WithTimeout calls WithDeadline internally. The only difference is that WithTimeout means XXX will time out from now on, while WithDeadline can be the previous time: meaning WithTimeout means XXX will time out from now on. WithDeadline means xx, end! This time point can be yesterday, there is no limit to the time point.

Here’s an example from GoDoc:

WithDeadline

package main

import (
    "context"
    "fmt"
    "time"
)

func main(a) {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // Even though ctx will be expired, it is good practice to call its
    // cancelation function in any case. Failure to do so may keep the
    // context and its parent alive longer than necessary.
    defer cancel() // Time out is automatically called

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }

}

Copy the code

Output:

context deadline exceeded
Copy the code

WithTimeout

package main

import (
    "context"
    "fmt"
    "time"
)

func main(a) {
    // Pass a context with a timeout to tell a blocking function that it
    // should abandon its work after the timeout elapses.
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel() // Time out is automatically called

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"}}Copy the code

Output:

context deadline exceeded
Copy the code

Irrevocable context, passing the value

WithValue can be used to pass the value, the value is accessed in KV form. Let’s go straight to the example

type favContextKey string

    f := func(ctx context.Context, k favContextKey) {
        ifv := ctx.Value(k); v ! =nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }

    k := favContextKey("language")
    k1 := favContextKey("Chinese")
    ctx := context.WithValue(context.Background(), k, "Go")
    ctx1 := context.WithValue(ctx, k1, "Go1")

    f(ctx1, k1)
    f(ctx1, k)
Copy the code

Output:

found value: Go1
found value: Go
Copy the code

For more exciting content, please follow my wechat official accountInternet Technology NestOr add wechat to discuss and exchange: