Get started quickly with GO concurrent programming

This article starts from actual combat and introduces the concurrency practice of GO language from the two perspectives of competition between threads and collaboration between threads . Finally, it focuses on the use of channels in GO, which is the essence of GO language concurrency. let's start!

1 Competition between threads

When multiple threads perform competing operations on shared data in the same high-level language , data inconsistency will result. Using "locks" can avoid using shared data at the same time and can solve competition problems. But the incorrect use of "locks", such as deadlock , is a new problem. In the GO language, the existence of "locks" is supported, mainly including mutex locks and read-write locks. GO also supports other methods to avoid competition. The mainstream ones include atomic operations, MAP locks, etc.

mutex lock

goroutineMutex locks ensure that only one can access shared data at the same time . Go language uses syncpackages to implement mutex locks. A classic use example is as follows:

package main

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

type test struct {
	x int
	mutex sync.Mutex
}

func (t *test) add(){
	//对t.x++操作进行加锁,避免竞争。
	//如果不加锁,会出现错误的结果
	t.mutex.Lock()
	t.x++
	t.mutex.Unlock()
}

func (t *test) get()int{
	return t.x
}

func main() {
	t:=test{x: 0}
	for i := 0; i < 1000; i++ {
	//并发的运行添加操作
		go t.add()
	}
	//这里之所以加休止,是因为main函数也是一个特殊的go程。所以很有可能前面开启的go程还没运行完,main程已经选择输出了,也会出现错误的结果。
	//此处也可以使用WaitGroup方法,后面会介绍。
	time.Sleep(100)
	fmt.Println(t.get())
}

read-write lock

In the case of more reading and less writing, read-write locks can be used first . The usage of read-write lock and mutex lock is very similar. The read lock allows shared resources to be read arbitrarily but cannot be written. The write lock is a mutex lock. The sync library is used in the GO language to provide read and write locks. A classic use example is as follows:

package main

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

// 声明读写锁
var rwlock sync.RWMutex
var x int

// 写数据
func write() {
	rwlock.Lock()
	x += 1
	rwlock.Unlock()
}

func read(i int) {
	rwlock.RLock()
	fmt.Println(x)
	rwlock.RUnlock()
}

func main() {
	go write()
	for i := 0; i < 1000; i++ {
		go read(i)
	}
	time.Sleep(100)
}

map lock

The map in GO supports concurrent reading, but it does not support concurrent writing. However, you can use syncthe map that supports concurrency to achieve concurrency. The sample code is as follows:

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	var m = sync.Map{}

	// 并发写
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			m.Store(i,i*i)
			}(i)
	}
	wg.Wait()
	fmt.Println(m.Load(1))
}

Atomic operations

You can also use sync/atomiclibraries to complete atomic operations to ensure thread safety. Atomic operations can be completed in user mode, and their performance is higher than locking. There are mainly the following atomic methods:

//加减方法
AddXxx()
//读取方法
LoadXxx()
//写入方法
StoreXxx()
//交换方法
SwapXxx()

2 Cooperation between threads

There is not only a competitive relationship between threads, but also a cooperative relationship. For example, the consumer/producer model requires cooperation between different threads. The method of realizing collaboration in the GO language can not only use traditional methods such as condition variables, but also achieve collaboration through the unique channel mechanism. Next, WaitGroup, condition variables, and channels will be introduced in sequence.

WaitGroup

WaitGroup can be used in GO to achieve synchronization between multiple GO processes. Within sync.WaitGroupa type, each sync.WaitGroup value internally maintains a count with an initial default value of zero. This counter can be changed by calling the following methods.

method name Function
Add(x int) Wait group counter +1
Done() Counter of waiting group -1
Wait() When the wait group counter is not equal to 0, block until it becomes 0.

It is usually used as the main function GO process to wait for other GO processes to complete. It is simply used as follows:

package main

import (
	"fmt"
	"sync"
)
//声明一个计数器
var wg sync.WaitGroup

type test struct {
	x int
	mutex sync.Mutex
}

func (t *test) add(){
	//运行完毕 计数器减一
	defer wg.Done()
	//对t.x++操作进行加锁,避免竞争。
	//如果不加锁,会出现错误的结果
	t.mutex.Lock()
	t.x++
	t.mutex.Unlock()
}

func (t *test) get()int{
	return t.x
}

func main() {
	t:=test{x: 0}
	for i := 0; i < 1000; i++ {
	   //运行一个新GO程,计数器加一
		wg.Add(1)
	   //并发的运行添加操作
		go t.add()
	}
	//直到计数器为0,不然会一直阻塞
	wg.Wait()
	fmt.Println(t.get())
}

condition variable

Condition variables are usually used together with mutex locks to implement the wait/wakeup model and are an important part of collaboration. It can block a thread until it gets an external wake-up signal and then run it again. The condition variables in GO are based on locks and syncimplemented in the library. The following is a simple example of condition variables in GO.

package main

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

var wg sync.WaitGroup
var cond *sync.Cond

func test(x int) {
	defer wg.Done()
	cond.L.Lock() // 获取锁
	cond.Wait()   // 等待通知 暂时阻塞
	fmt.Println(x)
	time.Sleep(time.Second * 1)
	cond.L.Unlock()
}

func main() {
	cond = sync.NewCond(&sync.Mutex{})
	fmt.Println("start all")
	for i := 0; i < 40; i++ {
		wg.Add(1)
		go test(i)
	}
	time.Sleep(time.Second * 3)
	fmt.Println("one")
	cond.Signal() // 下发一个通知给已经阻塞的goroutine
	time.Sleep(time.Second * 3)
	fmt.Println("one")
	cond.Signal() // 3秒之后 下发一个通知给已经阻塞的goroutine
	time.Sleep(time.Second * 3)
	fmt.Println("broadcast")
	cond.Broadcast() //3秒之后 下发广播给所有等待的goroutine
	wg.Wait()
}

3 channel

Don’t communicate through shared memory, share memory through communication

The locks and atomic operations introduced above are supported by most programming languages. Next, the concurrency core component channel (pipeline) of the GO language will be introduced.

Channels and Goroutines complement each other. As a unique concurrency of GO, GO processes are user-mode coroutines. They are very lightweight and can easily run thousands of GO processes on a computer with average performance. The main function of Channel is to serve as a communication channel to provide a communication bridge between GO processes. It has the function of receiving and sending data , similar to a global concurrent and safe queue. GO language designers emphasize not to communicate through shared memory, but to share memory through communication . This is the design idea of ​​GO language. The use of channels will be introduced next.

Define and create a channel

The definition of channel is very similar to map. First of all, the data structure in the channel can be defined by yourself. For example, the following code defines a channel that can store Int type.

var ch chan int

Pay attention to the above code and pay attention to a few points:

  • After defining ch as chan of type int, ch is still a nil value and cannot be used directly! Just like the use of map, you need to use .
  • Not only can you define chan of type int, but you can also define chan of other types.

The creation of channel is also very similar to map. It can be created using the following code:

ch=make(chan int,10)

Notice:

  • The type specified when creating must be consistent with the type when defining. Chan can be used normally only after it is created.
  • When creating, you can specify the size of the channel , in the above example it is 10. When the size is 0, the created channel is an unbuffered channel , and when it is greater than 0, the created channel is buffered .

Of course, you can also put definition and creation together:

ch:=make(chan int,10)

Basic operations of channels

Channel has the function of sending data and receiving data. The writing methods are as follows:

x := 0
//创建通道ch
ch:= make(chan int,1)
// 向ch发送数据
ch <- 1
// 从ch接收数据
x = <- ch

Notice:

  • Each channel has a buffer size. When the buffer is full, sending will be blocked, and when the buffer is empty, reception will be blocked.
  • A single send and a single receive of an unbuffered channel will be blocked, and the operation can continue only when the send and receive pairs exist.

According to the operation classification of channels, pipelines can also be divided into ordinary channels and one-way channels. The definitions are as follows:

//只能向通道发送数据
var ch chan-< int
//只能从通道接受数据
var ch <-chan int  
  • Generally speaking, when passing a channel as a function value to a function, a one-way channel is used, which limits the function's operations on the channel.

Next, write an example of two people playing table tennis to demonstrate the usage of channel:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var wg sync.WaitGroup

func A(ch chan int) {
	defer wg.Done()
	for {
		time.Sleep(time.Second)
		//得到击球机会
		num := <-ch
		//如果是有效的回球
		if num >= 1 {
			//产生随机数回球,如果数小于300,代表没打到球,输掉比赛。
			temp := rand.Int63() % 1000
			fmt.Println("A击出第", num, "球,得到", temp, "分!")
			if temp < 300 {
				fmt.Println("A输掉比赛!")
				ch <- 0
				break
			}
			ch <- num + 1
		} else {
			//如果是无效的球,退出循环
			break
		}
	}
	fmt.Println("A结束比赛~")
}
//B和A类似
func B(ch chan int) {
	defer wg.Done()
	for {
		time.Sleep(time.Second)
		//得到击球机会
		num := <-ch
		if num >= 1 {
			temp := rand.Int63() % 1000
			fmt.Println("B击出第", num, "球,得到", temp, "分!")
			if temp < 300 {
				fmt.Println("B输掉比赛!")
				ch <- 0
				break
			}
			ch <- num + 1
		} else {
			break
		}
	}
	fmt.Println("B结束比赛~")
}

func main() {
	ch := make(chan int)
	wg.Add(2)
	//启动双方
	go A(ch)
	go B(ch)
	//发球
	ch <- 1
	//等待比赛结束
	wg.Wait()
}

select

After learning the basic operations of chan, you still need to learn a very important thing - select. The syntax of select and switch are very similar, but the usage methods are completely different. It can be understood as a switch dedicated to communication , and each case must be a channel communication operation (receive or send).

Note: The Go language selectstatement is borrowed from the Unix select()function. In Unix, you can select()monitor a series of file handles by calling the function. Once an IO action occurs on one of the file handles, the select()call will be returned (this is the case in C language (done), and later this mechanism was also used to implement high-concurrency Socket server programs. The Go language directly supports keywords at the language level selectto handle asynchronous IO communication issues between channels in concurrent programming. ——"Huben GO (C Language in the 21st Century)"

The following is the basic syntax:

select {
    case <-ch:
        // 如果从 ch 信道成功接收数据,则执行该分支代码
    case ch1 <- 1:
        // 如果成功向 ch1 信道成功发送数据,则执行该分支代码
    // 你可以定义任意数量的 case 
    //default为可选项
    default : 
     //如果没有case可运行,则运行此分支
}

Pay attention to a few points:

  • Each case should be followed by a communication operation about chan .

  • Not all expressions following the case will be executed . If there are multiple runnable cases , select will randomly execute a runnable case and execute the branch code below it.

  • If there is no case to run, the default branch will be run. If there is no default branch, it will block until there is a case to run.

select and timer

Select is often used together with timers to perform certain operations on a regular basis. It is usually used to monitor whether a service is completed within a specified time. Here is a simple code that you can try to modify:

package main

import (
	"fmt"
	"time"
)

func A(ch chan int) {
	time.Sleep(time.Second * 2)
	ch <- 1
}

func main() {
	ch := make(chan int)
	go A(ch)
	select {
	//接受来自ch的消息
	case <-ch:
		fmt.Println("接收到消息!")
		//定时器设置为1s,超过1s后case被激活
	case <-time.After(1 * time.Second):
		fmt.Println("没接收到消息!")
	}
}

Summarize

In this article, we mainly introduce the basic syntax of GO language concurrency and give some small demos. The author's GO language column also has some practical examples of advanced versions. Interested friends can go to the homepage and search for "GO concurrency" to view.

Guess you like

Origin blog.csdn.net/doreen211/article/details/125066246