[Golang] Quick Review Guide QuickReview - goroutine

Goroutines are unique to Golang, similar to threads, but threads are scheduled and managed by the operating system, while goroutine s are user-mode threads scheduled and managed by the Golang runtime.

1. Thread operation in C#

1.1 Create a thread

 static void Main(string[] args)
 {
     Thread thread = new Thread(Count);
     thread.IsBackground = true;
     thread.Start();
     for (int i = 0; i < 10; i++)
         Console.Write("x\n");
 }
 static void Count()
 {
     for (int i = 0; i < 100; i++)
     {
         Console.WriteLine(i); ;
     }
 }
copy

1.2 Passing parameters to threads

The Thread constructor has two parameters ParameterizedThreadStart and ThreadStart

public delegate void ParameterizedThreadStart(object? obj);
public delegate void ThreadStart();
copy

That's right, one is a no-parameter delegate, and the other is a delegate with parameters and the parameter type is object, so the method parameter we use to create a thread needs to be of object type, and then internally convert the object type parameter:

static void Main(string[] args)
{
    Thread thread = new Thread(Count);
    thread.IsBackground = true;
    thread.Start(100);
    for (int i = 0; i < 10; i++)
        Console.Write("x\n");
}


static void Count(object times)
{
    int count = (int)times;
    for (int i = 0; i < 100; i++)
    {
        Console.WriteLine(i); ;
    }
}
copy

Of course, the easiest way is to use lambda expressions directly

static void Main(string[] args)
{
    Thread thread = new Thread(()=>{
        Count(100);
    });
    thread.IsBackground = true;
    thread.Start();
    for (int i = 0; i < 10; i++)
        Console.Write("x\n");
}

static void Count(object times)
{
    int count = (int)times;
    for (int i = 0; i < 100; i++)
    {
        Console.WriteLine(i); ;
    }
}
copy

Note: Using Lambda expressions can easily pass parameters to Thread, but after the thread starts, the captured variables may be accidentally modified, which should be paid more attention. For example, in the body of a loop, it is best to create a temporary variable.

1.3 Thread Safety and Locks

Thread safety from the singleton pattern:

class Student
{
    private static Student _instance =new Student();
    private Student()
    {
    }
    static Student GetInstance()
    {
        return _instance;
    }
}
copy

**Singleton mode, our intention is to always keep the class instantiated once and ensure that there is only one instance in memory. **In the above code, when the class is loaded, the initialization of the static private variable is completed, whether it is needed or not, it will be instantiated. This is called the singleton mode of the hungry Chinese mode. In this way, although there is no thread safety problem, this class does not need to be instantiated if it is not used. Then there is the following way of writing: when needed, instantiate

class Student
{
    private static Student _instance;
    private Student()
    {
    }
    static Student GetInstance()
    {
        if (_instance == null) 
                        _instance = new Student();
        return _instance;
    }
}
copy

The above code, when called, judges whether the static private variable is empty, and then assigns it. This actually has a thread safety problem: when multiple threads call GetInstance(), when multiple threads execute at the same time, the condition _instance == null may be satisfied at the same time. In this way, _instance completes multiple instantiation assignment operations, which leads to our lock Lock

private static Student _instance;
private static readonly object locker = new object();
private Student()
{

}
static Student GetInstance()
{
    lock (locker)
    {
        if (_instance == null)
            _instance = new Student();
    }

    return _instance;
}
copy
  • The first thread to run will lock the Lock
  • The second thread runs and first detects that the locker object is in the "locked" state (whether there are still threads in the lock that have not been executed yet), the thread will block and wait for the first thread to unlock
  • After the first thread executes the code in the lock body and unlocks it, the second thread will continue to execute

The above seems to be perfect, but in the case of multi-threading, we have to go through, detect (blocking), lock, and unlock each time, which actually affects performance. Our original purpose is to return a single instance. ** Before we check whether the locker object is locked, if the instance already exists, then the follow-up work is unnecessary. **So there is the following double test:

private static Student _instance;
private static readonly object locker = new object();
private Student()
{

}
static Student GetInstance()
{
    if (_instance == null)
    {
        lock (locker)
        {
            if (_instance == null)
                _instance = new Student();
        }
    }

    return _instance;
}
copy

1.4 Task

Threads have two types of work:

  • CPU-Bound: Computationally intensive, operations that spend most of their time performing CPU-intensive work, this type of work never leaves threads in a wait state.
  • IO-Bound: I/O-intensive, an operation that spends most of its time waiting for something to happen, waits all the time, and causes a thread to enter a wait state. Such as requesting access to resources through http.

For the operation of IO-Bound, the time is mainly spent on I/O, and almost no time is spent on the CPU. For this, it is more recommended to use Task to write asynchronous code, and the asynchronous code of CPU-Bound and IO-Bound is not the focus of this article. The blogger will briefly introduce the advantages of Task:

  • No more waiting: Take HttpClient requesting GetStringAsync using asynchronous code as an example, this is obviously an I/O-Bound, which will eventually call the local network library of the operating system, system API calls, such as the socket that initiates the request, but this time The length is not determined by the code, it depends on the hardware, operating system, and network conditions. Control returns to the caller and we can do other things, which allows the system to do more work than waiting for the I/O call to finish. Until await goes to get the request result.
  • Let the caller stop waiting: With CPU-Bound, there is no way to avoid using a thread for computation, since it is a computationally intensive job after all. But asynchronous code using Task (async vs await) not only interacts with the background thread, but also lets the caller continue to respond (other operations can be performed concurrently). Ditto, an async method yields to the caller until await is encountered.

2. goroutine in Golang

2.1 Start goroutine

Starting a goroutine in Golang is not as troublesome as C# threads, just add the keyword go before the calling method.

func main(){
    go Count(100)
}
func Count(times int) {
 for i := 0; i < times; i++ {
  fmt.Printf("%v\n", i)
 }
}
copy

2.2 Synchronization of goroutines

Tasks in C# can use Task.WhenAll to wait for the Task object to complete. In Golang, it looks simple and rude, it will use the WaitGroup of the sync package, and then register it like a roster:

  • start - count +1
  • Executed - count -1
  • Finish
package main

import (
 "fmt"
 "sync"
)

var wg sync.WaitGroup

func main() {
    
    //register
 wg.Add(2)
 go Count(100)
 go Count(100)
    
    //Wait for all registered goroutine s to finish
 wg.Wait()
 fmt.Println("execution complete")
}

func Count(times int) {
    
    //execution complete
 defer wg.Done()
 for i := 0; i < times; i++ {
  fmt.Printf("%v\n", i)
 }
}
copy

2.3 channel channel

The communication mechanism for one goroutine to send a specific value to another goroutine is the channel

2.3.1 channel declaration

channel is a reference type

var variable chan element type
copy

The element type of chan, like struct and interface, is a type. The latter element type defines the specific storage type of the channel.

2.3.2 channel initialization

The declared channel is nil and needs to be initialized before it can be used.

var ch chan int
ch=make(chan int,5) //5 is the set buffer size, optional parameter
copy

2.3.3 channel operation

Operating the three axes:

  • Send - send ch<-100

Arrow points from value to channel

  • Receive - receive value:=<-ch
i, ok := <-ch1 // After the channel is closed, take the value ok=false

//or
for i := range ch1 { // The for range loop will exit after the channel is closed
    fmt.Println(i)
}
copy

The arrow points from the channel to the variable

  • close - close close(ch)

2.3.3 Buffered and unbuffered

ch1:=make(chan int,5) //5 is the set buffer size, optional parameter
ch2:=make(chan int)
copy

Unbuffered channels, unbuffered channels can only send values ​​when they are received.

  • Only pass values ​​to the channel, not receive from the channel, a deadlock will appear
  • Only receiving from the channel, not sending to the channel, will also block

Communication using an unbuffered channel will cause the sending and receiving goroutine s to be synchronized. Therefore, an unbuffered channel is also called a synchronous channel.

A buffered channel can effectively alleviate the embarrassment of an unbuffered channel, but when the channel is full, the above embarrassment still exists.

2.3.4 One-way channel

The channel can only be sent or received in the function, and the one-way channel is on the scene. The use of the one-way channel is in the parameters of the function, and no new keywords are introduced, but the position of the arrow is simply changed:

chan<- int write only, not read
<-chan int read only not write
copy

Function parameters and any assignment operations can convert bidirectional channels to unidirectional channels, and vice versa.

2.3.5 Multiplexing

package main

import "fmt"

func main() {
 ch := make(chan int, 1)
 for i := 0; i < 10; i++ {
  select {
  case x := <-ch:
   fmt.Printf("the first%v Second-rate,x := <-ch,read value from channel%v", i+1, x)
   fmt.Println()
  case ch <- i:
   fmt.Printf("the first%v Second-rate,implement ch<-i", i+1)
   fmt.Println()
  }
 }
}

copy
1st,implement ch<-i
 2nd,x := <-ch,read the value 0 from the channel
 the 3rd time,implement ch<-i
 4th,x := <-ch,read value 2 from channel
 5th,implement ch<-i
 6th time,x := <-ch,read value 4 from channel
 7th time,implement ch<-i
 8th time,x := <-ch,read value 6 from channel
 9th time,implement ch<-i
 10th time,x := <-ch,read value 8 from channel
copy

Rules for Select multiplexing:

  • Can handle send/receive operations on one or more channel s.
  • If multiple case s are satisfied at the same time, select will choose one at random.
  • For select{} without case, it will always wait and can be used to block the main function.

2.5 Concurrency Safety and Locks

Goroutines communicate through channel s. There will be no concurrency safety issues. However, in fact, it is still impossible to completely avoid the situation of operating public resources. If multiple goroutine s operate these public resources at the same time, concurrency security problems may occur. Like C# threads, the appearance of locks is to solve this problem:

2.5.1 Mutex

Mutual exclusion lock, this is the same as the C# lock mechanism, one goroutine access, the other can only wait for the release of the mutex lock. The sync package is also required:

sync.Mutex

var lock sync.Mutex

lock.Lock()//lock

//Manipulate public resources

lock.Unlock()//unlock
copy

2.5.2 Read-write mutex

Mutual exclusion locks are completely mutually exclusive. If you read more and write less, most of the goroutines are reading, and a small number of goroutines are writing. At this time, there is no need to add locks for concurrent reads. When using, the sync package is still required:

sync.RWMutex

There are two types of read-write locks:

  • read lock
  • write lock
import (
 "fmt"
 "sync"
)

var (
 lock   sync.Mutex
 rwlock sync.RWMutex
)

rwlock.Lock() // add write lock

//The effect is equivalent to a mutex lock
rwlock.Unlock() // unlock lock

rwlock.RLock()  //Add read lock

//Readable and not writable
rwlock.RUnlock() //read lock
copy

2.6* sync.Once

goroutine synchronization we have used sync.WaitGroup

  • Add(count int) counter is accumulated, executed outside the calling goroutine, specified by the developer
  • Done() counter -1, executed inside goroutine
  • Wait() blocks until the counter reaches 0

In addition to this, there is also a sync.Once, as the name suggests, once, only executed once.

func (o *Once) Do(f func()) {}

var handleOnce sync.Once
handleOnce.Do(function)
copy

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

type Once struct {
 // done indicates whether the action has been performed.
 // It is first in the struct because it is used in the hot path.
 // The hot path is inlined at every call site.
 // Placing done first allows more compact instructions on some architectures (amd64/x86),
 // and fewer instructions (to calculate offset) on other architectures.
 done uint32
 m    Mutex
}
copy

2.7* sync.Map

Golang's map is not concurrency safe. An out-of-the-box concurrency-safe version of map–sync.Map is provided in the sync package.

Out-of-the-box means that it can be used directly without initializing it with the make function like the built-in map. At the same time, sync.Map has built-in operation methods such as Store, Load, LoadOrStore, Delete, and Range.

var m = sync.Map{}
m.Store("Sichuan", "Chengdu")
m.Store("Chengdu", "High-tech Zone")
m.Store("High-tech Zone", "Yinglong South Road")
m.Range(func(key interface{}, value interface{}) bool {
    fmt.Printf("k=:%v,v:=%v\n", key, value)
    return true
})
fmt.Println()
v, ok := m.Load("Chengdu")
if ok {
    fmt.Println(v)
}
fmt.Println()
value, loaded := m.LoadOrStore("Shaanxi", "Xi'an")
fmt.Println(value)
fmt.Println(loaded)
m.Range(func(key interface{}, value interface{}) bool {
    fmt.Printf("k=:%v,v:=%v\n", key, value)
    return true
})
fmt.Println()

//[value and creation]
//Load it if it exists, add it if it doesn't exist
value1, loaded1 := m.LoadOrStore("Sichuan", "Chengdu")
fmt.Println(value1)
fmt.Println(loaded1)
m.Range(func(key interface{}, value interface{}) bool {
    fmt.Printf("k=:%v,v:=%v\n", key, value)
    return true
})
fmt.Println()

//[delete]
//Load and delete key exists
value2, loaded2 := m.LoadAndDelete("Sichuan")
fmt.Println(value2)
fmt.Println(loaded2)
m.Range(func(key interface{}, value interface{}) bool {
    fmt.Printf("k=:%v,v:=%v\n", key, value)
    return true
})
fmt.Println()

//Load and delete key does not exist
value3, loaded3 := m.LoadAndDelete("Beijing")
fmt.Println(value3)
fmt.Println(loaded3)
m.Range(func(key interface{}, value interface{}) bool {
    fmt.Printf("k=:%v,v:=%v\n", key, value)
    return true
})
fmt.Println()

m.Delete("Chengdu")  //Inside is the call to LoadAndDelete

//[traversal]
m.Range(func(key interface{}, value interface{}) bool {
    fmt.Printf("k=:%v,v:=%v\n", key, value)
    return true
})
fmt.Println()
copy
k=:Sichuan,v:=Chengdu
k=:Chengdu,v:=High-tech Zone
k=:High-tech Zone,v:=Yinglong South Road

High-tech Zone

Xi'an
false
k=:Sichuan,v:=Chengdu
k=:Chengdu,v:=High-tech Zone
k=:High-tech Zone,v:=Yinglong South Road
k=:Shaanxi,v:=Xi'an

Chengdu
true
k=:Sichuan,v:=Chengdu
k=:Chengdu,v:=High-tech Zone
k=:High-tech Zone,v:=Yinglong South Road
k=:Shaanxi,v:=Xi'an

Chengdu
true
k=:Shaanxi,v:=Xi'an
k=:Chengdu,v:=High-tech Zone
k=:High-tech Zone,v:=Yinglong South Road

<nil>
false
k=:Chengdu,v:=High-tech Zone
k=:High-tech Zone,v:=Yinglong South Road
k=:Shaanxi,v:=Xi'an

k=:High-tech Zone,v:=Yinglong South Road
k=:Shaanxi,v:=Xi'an
copy

2.8 Singleton Pattern

To sum up, sync.Once actually contains a mutex and a boolean value. This boolean value is equivalent to the first judgment of the double check in the C# singleton mode. So in golang you can use sync.Once to implement the singleton pattern:

package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}
copy

2.9* Atomic operations

The locking operation in the code will be time-consuming and expensive because of the context switching involved in the kernel state. For basic data types we can also use atomic operations to ensure concurrency safety. Atomic operations in Golang are provided by the built-in standard library sync/atomic. Since there are few scenes, no introduction will be given. For detailed operations, please read and learn by yourself.

Posted by crisward on Fri, 01 Jul 2022 21:51:51 +0530