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
copystatic 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); ; } }
1.2 Passing parameters to threads
The Thread constructor has two parameters ParameterizedThreadStart and ThreadStart
copypublic delegate void ParameterizedThreadStart(object? obj); public delegate void ThreadStart();
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:
copystatic 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); ; } }
Of course, the easiest way is to use lambda expressions directly
copystatic 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); ; } }
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:
copyclass Student { private static Student _instance =new Student(); private Student() { } static Student GetInstance() { return _instance; } }
**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
copyclass Student { private static Student _instance; private Student() { } static Student GetInstance() { if (_instance == null) _instance = new Student(); return _instance; } }
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
copyprivate 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; }
- 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:
copyprivate 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; }
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.
copyfunc main(){ go Count(100) } func Count(times int) { for i := 0; i < times; i++ { fmt.Printf("%v\n", i) } }
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
copypackage 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) } }
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
copyvar variable chan element type
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.
copyvar ch chan int ch=make(chan int,5) //5 is the set buffer size, optional parameter
2.3.3 channel operation
Operating the three axes:
- Send - send ch<-100
Arrow points from value to channel
- Receive - receive value:=<-ch
copyi, 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) }
The arrow points from the channel to the variable
- close - close close(ch)
2.3.3 Buffered and unbuffered
copych1:=make(chan int,5) //5 is the set buffer size, optional parameter ch2:=make(chan int)
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:
copychan<- int write only, not read <-chan int read only not write
Function parameters and any assignment operations can convert bidirectional channels to unidirectional channels, and vice versa.
2.3.5 Multiplexing
copypackage 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() } } }
copy1st,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
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
copyvar lock sync.Mutex lock.Lock()//lock //Manipulate public resources lock.Unlock()//unlock
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
copyimport ( "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
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()) {}
copyvar handleOnce sync.Once handleOnce.Do(function)
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.
copytype 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 }
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.
copyvar 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()
copyk=: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
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:
copypackage singleton import ( "sync" ) type singleton struct {} var instance *singleton var once sync.Once func GetInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance }
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.