Learn the necessary concurrent knowledge

channel

  • As mentioned earlier, channel is a reference type and can only be used after the make function is initialized
chan1 := make(chan int)
chan2 := make(chan int, 10 )
  • A with a number indicates that it has a cache

Channels with cache and channels without cache:

The channel without cache must be received by someone. The channel with cache can be put in. When it is full, it will be blocked.

select keyword

  • For scenarios where there are multiple channels to operate at the same time, select is used to realize multiplexing
func main() {
	ch := make(chan int, 1)
	for i := 1; i <= 10; i++ {
		select {
		case ch <- i:
		case x := <-ch:
			fmt.Println(x)
		}
	}
}   // Answer: 1 3 5 7 9

  • The answer is fixed, because its cache is 1. If you enter one, you can't proceed. You can only wait
  • If the cache is larger, the answer is unpredictable.

The select statement can improve the readability of the code:

  • It can handle the receive / send operation of one or more channel s
  • If multiple case s are satisfied at the same time, select will randomly select one
  • For select {} without case operation, it will wait all the time and can be used to block the main function.

Application of sync.WaitGroup

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	jobChan := make(chan int, 100)
	resultChan := make(chan int, 100)

	// Continuous input data
	go func() {
		for i := 1; i <= 100; i++ {
			jobChan <- i
			time.Sleep(time.Millisecond * 100)
		}
		close(jobChan)
	}()

	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for v := range jobChan {
				resultChan <- v
			}
		}()
	}
	go func() {
		for i := range resultChan {
			fmt.Println(i)
		}
	}()
	wg.Wait()
	close(resultChan)

}
  • In addition to sync.WaitGroup, we can also use notification to solve the problem of shutdown, because if we don't shut down, the program will keep reading values, resulting in deadlock

notify notification is used, which contains an empty anonymous structure

package main

import (
	"fmt"
	"time"
)

func main() {
	jobChan := make(chan int, 100)
	resultChan := make(chan int, 100)
	notityChan := make(chan struct{}, 100)

	// Continuous input data
	go func() {
		for i := 1; i <= 100; i++ {
			jobChan <- i
			time.Sleep(time.Millisecond * 100)
		}
		close(jobChan)
	}()

	for i := 0; i < 10; i++ {
		go func() {
			for v := range jobChan {
				resultChan <- v
				notityChan <- struct{}{}
			}
		}()
	}

	go func() {
		for i := 0; i < 100; i++ {
			<-notityChan
		}
		close(resultChan)
	}()

	for i := range resultChan {
		fmt.Println(i)
	}

}

  • It's more convenient to use anonymous empty structures, mainly because they don't occupy space and are old and comfortable,
  • It can also solve the deadlock problem

mutex

  • When multiple threads run together, some concurrent errors may occur. At this time, we need to use the mutex lock to solve the problem
  • Let's take a wrong example

  • You can see that the answer is only 13592, which should be 20000. Why is there so much data missing? It is because there is a concurrency error when two public user threads go to get public resources during concurrency, resulting in only getting them once.
  • How to solve it?

Mutex:

  • Mutex is a common method to control access to public resources. It can ensure that only goroutine can access shared resources at the same time.

==Mutex is implemented through mutex type in sync package, and the usage is quite simple.

lock.Lock()  // Lock
 Code block
lock.Unlock()  // Unlock

  • After locking, there will be no concurrency error.

Read write mutex

  • The above mutexes are completely mutually exclusive, but there are many actual scenarios where there are more reads and less writes
  • When our program reads a resource concurrently, there will be no concurrency error, that is, there is no need to lock. In this scenario, using read-write lock is a better choice.
  • The RWMutex type in the sync package is used for the read / write lock in the go language

There are two kinds of read-write locks: read lock and write lock

When a goroutine acquires a read lock, other goroutines will continue to acquire the lock if they acquire a read lock, and wait if they acquire a write lock;

When a goroutine acquires a write lock, other goroutines will wait regardless of whether they acquire a read lock or a write lock

  • In this way, the operation will be much faster than the direct mutex
  • Here we can test, set a public resource x, test it with mutex first, and then test it with read-write

Mutex program test

func read() {
	defer wg.Done()
	lock.Lock()
	fmt.Println(x)
	time.Sleep(time.Millisecond) // Suppose reading takes 1 ms
	lock.Unlock()
}

func write() {
	defer wg.Done()
	lock.Lock()
	x += 1
	time.Sleep(time.Millisecond * 10) // Writing takes 10 milliseconds
	lock.Unlock()
}
  • Set two mutually exclusive locks

  • As you can see, ha, it took 1.8900291s to write 10 times and read 1000 times
  • Then let's take a look at the read-write lock. Remember, use the RWMutex type

  • Open your eyes, oh, only 113.9215ms, only a hundred milliseconds, brothers.
  • The greater the number of reads, the more comfortable it is, but the greater the number of writes. Ha ha ha ha ha ha
  • Because I read too fast, I didn't finish reading public resources. emmmm this should be paid attention to.

sync.Once

  • In many programming scenarios, we need to ensure that certain operations are performed only once in high concurrency scenarios, such as loading the configuration file only once, closing the channel only once, and so on.
  • For such scenarios, the Once type of sync is provided in go
  • There is only one Do method in this type. You need to pass in a function
func (o *Once) Do(f func()) {}
  • If you need to force it to run, you need to use closures.

The init method of the program itself is that when the program starts, init will start with it, but sometimes we don't need this initialization operation, and it also loads, which virtually consumes our performance.

  • So the advantage of Once comes. When such initialization is not used in the program, it will not be loaded. When it is used, it will be loaded by the program.

You may think that you can add an if directly to judge whether it is empty and load it soon!

  • Yes, it is reasonable to say that there is no problem in the case of serial, but when concurrent, it will be loaded many times.
  • When loading through Once, it not only ensures that it will be loaded when it is used, but also ensures the safety of the thread.
package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}
  • Here's the whole case model of concurrency security.

sync.Once actually contains a mutex and a Boolean value. The mutex ensures the security of Boolean values and data, and the Boolean value is used to record whether the initialization is completed. This design can ensure that the initialization operation is concurrent and safe, and the initialization operation will not be executed many times.

sync.map

  • First of all, we should know that maps are widely used in our daily development, but the built-in map in go is not safe. When there is high concurrency, there will be many concurrency errors.
  • for instance:
var m = make(map[string]int)

func get(key string) int {
	return m[key]
}

func set(key string, value int) {
	m[key] = value
}
  • Here we read and write an ordinary map

  • Then read and write concurrently, and the program will explode directly

This is because:
The Map built in go language will compete for shared variables, resources and concurrent writing, and the shared resources will be destroyed

  • At this time, the function of sync.map comes
  • This is the sync package of Go language, which provides a concurrent secure version of map – sync.Map out of the box
  • Out of the box means that you can use it directly without using the make function initialization like the built-in map. Meanwhile, sync.Map has built-in operation methods such as Store, Load, LoadOrStore, Delete and Range.

  • The built-in methods in the synchronization map can be used directly. Comfort + 1

There are two ways to remember

m.Store(key, value)

Value, OK: = M. load (key) is used to read data

  • These two must be written down

Atomic operation

  • This is the operation provided by the atomic package. It is available for all types.
  • This is similar to the atomic package in java, which provides the atomic operation of a variable

What is atomic operation?

  • That is, the reading, modification and assignment of a variable are regarded as a whole, either all succeed or all fail.
  • We need the essence of concurrency, that is, there is no atomic operation

The use is also very simple. Look at the official documents and start

  • Here we give a simple example
  • A simple operation of adding a variable by itself will make mistakes in high concurrency, but it won't happen if you use atomic self addition
atomic.AddInt64(&x,1)
  • That's it. It's simple

go language development document (Chinese version)

  • Slip, slip, stick to it!!!!

Tags: Go Back-end

Posted on Wed, 27 Oct 2021 11:56:14 -0400 by saunders1989