[Go language from entry to actual combat] concurrent articles

Go language from entry to actual combat - concurrent articles

coroutine

Thread vs Groutine

image-20230505105233850

In contrast, the stack size of the coroutine is much smaller, and it will be created faster and save system resources.

A goroutine's stack, like an operating system thread, will store local variables of its active or suspended function calls, but unlike an OS thread, the stack size of a goroutine is not fixed; the stack size will be based on It needs to be dynamically scaled, and the initial size is 2KB. The maximum value of the goroutine stack 1GBis much larger than the traditional fixed-size thread stack, although in general, most goroutines do not need such a large stack.

image-20230505105306798

So what does this many-to-many correspondence mean for our program?

If it is 1:1, then our thread (Thread) is directly scheduled by our kernel entity. In this way, its scheduling efficiency is very high, but there is a problem here. If a context switch occurs between threads, it will Involving the mutual switching of kernel objects, this will be a very expensive thing.

Relatively speaking, if multiple coroutines are scheduled by the same kernel entity, then switching between coroutines does not involve switching between kernel objects, and can be done internally, and the switching between them will be much smaller. Go is the main focus of this aspect.

image-20230505105449883

Scheduling mechanism in Go

Go's coroutine processor P (Processor) is hung under the system thread M (System thread), and there is a coroutine queue (Goroutine) ready to run under the coroutine processor P. Every time in each coroutine queue There is a coroutine G that is running.

If the execution time of the running coroutine is particularly long, will it block the coroutine queue?

The processing mechanism of Go is like this. When Go is running a coroutine, it will start a 守护线程counter to count the number of coroutines completed by each Processor. When it finds that the number of coroutines completed by a Processor has not changed after a period of time, It will insert a special mark into the task stack of the coroutine. When the coroutine runs and encounters a non-inline function, it will read this mark, interrupt itself, and insert it at the end of the coroutine queue. Then switch to the next coroutine to continue running.

Another concurrency mechanism is this. When a coroutine is interrupted by the system, for example, when I/O needs to wait, in order to improve the overall concurrency, the Processor will move itself to another available system thread and continue execution Other coroutines in the coroutine queue to which it hangs. When the coroutine that was interrupted last time is awakened again, it will add itself to one of the Processor's waiting queues, or the global waiting queue. During the interruption of the coroutine, its running state in the register will be saved in the coroutine object. When the coroutine has a chance to run again, the data will be written into the register again, and then continue to run.

In general, we can know the relationship between this coroutine mechanism and system threads 多对多, and how it efficiently utilizes system threads to run as many concurrent coroutine tasks as possible.

The first: scheduling in the case of channel blocking or network I/O

If G is blocked on a channel operation or network I/O operation, G will be placed in a waiting (wait) queue, and M will try to run P's next runnable G. If P has no runnable G for M to run at this time, then M will unbind P, and M will enter the suspended state. When the I/O operation is completed or the channel operation is completed, the G in the waiting queue will be woken up, marked as runnable, and put into the queue of a certain P, bound to an M to continue execution.

The second type: Scheduling in the case of system call blocking

If G is blocked on a system call (system call), then not only G will be blocked, but M that executes this G will also unbind P, and M and G will enter the suspended state together. If there is an idle M at this time, then P will bind to it and continue to execute other Gs; if there is no idle M, but there are still other Gs to execute, then the Go runtime will create a new M (thread ).

When the system call returns, G blocked on this system call will try to obtain an available P, if there is no available P, then G will be marked as runnable (if there is no available P, after a certain number of rounds, G will be is put into the global P), the previous suspended M will enter the suspended state again (M will enter the free list after a period of time, and obtain the available P again).

image-20230505105523193

For detailed analysis, please refer to this article: Go coroutine (goroutine) scheduling principle

Use of Go coroutines

The use of Go coroutines is very simple, just add a gokeyword in front of the method.

// Go 协程的使用
func TestGoroutine(t *testing.T) {
    
    
	for i := 0; i < 5; i++ {
    
    
		// 加到匿名函数前
		go func(i int) {
    
    
			fmt.Println(i)
		}(i)
	}
	time.Sleep(time.Millisecond * 50) // 让上面的程序先全部执行完
}

image-20230507193018030

The running result is similar to java creating multiple threads, the order in which the coroutines are called is not scheduled according to the order of the methods.

shared memory concurrency

Lock

image-20230507193728581

Not thread safe

func TestCounter(t *testing.T) {
    
    
	counter := 0
	for i := 0; i < 5000; i++ {
    
    
		go func() {
    
    
			counter++ // 创建5000个协程,对counter自增了5000次 预期值为5000
		}()
	}
	time.Sleep(1 * time.Second) // 使上面的程序先执行完
	t.Logf("counter = %d", counter)
}

image-20230507194513259

It did not meet our 5000 expectations. This is because the counter we use competes between different coroutines, resulting in concurrent competition, that is, non-thread-safe programs, and invalid write operations. If we want to ensure it thread safety, you need to lock this shared memory.

Thread-safe sync.Mutex

func TestCounterSafe(t *testing.T) {
    
    
	var mut sync.Mutex
	counter := 0
	for i := 0; i < 5000; i++ {
    
    
		go func() {
    
    
            // 锁的释放我们一般要写在defer中,类似java的finally。
			defer func() {
    
    
				mut.Unlock() // 在这个协程执行完的最后释放锁
			}()
			mut.Lock() // 加锁
			counter++
		}()
	}
	time.Sleep(1 * time.Second) // 使上面的程序先执行完
	t.Logf("counter = %d", counter)
}

image-20230507195002194

Meet our 5000 expectations.

WaitGroup

joinThe method of synchronizing each thread is equivalent to , in java CountDownLatch.

Only after all the content of my wait is completed, the program can continue to execute downward.

func TestCounterWaitGroup(t *testing.T) {
    
    
	var mut sync.Mutex
	var wg sync.WaitGroup
	counter := 0
	for i := 0; i < 5000; i++ {
    
    
		wg.Add(1) // 每启动1个协程,WaitGroup的数量就+1
		go func() {
    
    
			// 锁的释放我们一般要写在defer中,类似java的finally。
			defer func() {
    
    
				mut.Unlock() // 在这个协程执行完的最后释放锁
			}()
			mut.Lock() // 加锁
			counter++
			wg.Done() // 每执行完1个协程,WaitGroup的数量就-1
		}()
	}
	wg.Wait() // 如果WaitGroup中的数量不为0则一直等待
	t.Logf("counter = %d", counter)
}

image-20230507200751199

So why is WaitGroup better? You can look at the final execution time. If you use time.Sleep(), because we don’t know how long it will take to execute 5000 coroutines. This time is not easy to control. In order to get the correct result, we Artificially estimated 1 second, but in fact it only takes 0.00 seconds to complete the execution, so using WaitGroup can prevent wrong estimation of the execution time of the coroutine and ensure thread safety, which is the best choice.

RWLock read-write lock

It separates read locks and write locks. Reads are not mutually exclusive, and writes are mutually exclusive. It is more efficient than Mutex for complete mutual exclusion. It is recommended to use read-write locks.

CSP concurrency mechanism

CSP (Communicating Sequential Processes) communication sequence process is a message passing model that passes data between Goroutines through channels to pass messages, instead of locking data to achieve synchronous access to data.

CSP VS Actor

Actor Model

image-20230510142119686

  • The mechanism of Actor is to communicate directly, while the CSP mode communicates through channels , which is more loosely coupled.
  • Actors and Erlang use mailboxes to store messages. The capacity of mailboxes is unlimited, while the capacity of Go channels is limited.
  • Actor and Erlang's receiving process always process messages passively, while Go's coroutines will actively process messages transmitted from the channel.

image-20230510143143131

Channel

Typical Messaging Mechanism

The sender and receiver of the communication must be on the channel at the same time to complete this interaction, and the absence of either party will cause the other party to block and wait.

image-20230510143410224

buffer channel mechanism

Under this mechanism, the sender and receiver of the message are a more loosely coupled mechanism. We can set a capacity for the channel. As long as the capacity is not full, the person who puts the message can send the message Put in, if the capacity is full, you need to block and wait until the person who receives the message takes a message, and the person who put the message can continue to put it in. In the same way, for those who receive messages, as long as there are messages in this channel, they can keep getting them until there is no more messages in the channel, and they will block and wait until new messages come in.

image-20230510143421732

return asynchronously

When we call a task, we don't need to get its return result right away, we can execute other logic first, until we need the result, then get the result. This will greatly reduce the overall running time of the program and improve the efficiency of the program . If we get the result of this task, but the result of the task has not come out, it will be blocked there until we get the result.

java code

image-20230510143647616

synchronous (serial execution)

func service() string {
    
    
	time.Sleep(time.Millisecond * 50)
	return "service执行完成"
}

func otherTask() {
    
    
	fmt.Println("otherTask的各种执行逻辑代码")
	time.Sleep(time.Millisecond * 100)
	fmt.Println("otherTask执行完成")
}

// 测试同步执行效果, 先调用 service() 方法,在调用 otherTask() 方法,
// 理论上最后程序的执行时间为二者相加。
func TestService(t *testing.T) {
    
    
	fmt.Println(service())
	otherTask()
}

image-20230510144123221

0.15s,In line with expectations.

Typical channel returns asynchronously

func service() string {
    
    
	time.Sleep(time.Millisecond * 50)
	return "service执行完成"
}

func otherTask() {
    
    
	fmt.Println("otherTask的各种执行逻辑代码")
	time.Sleep(time.Millisecond * 100)
	fmt.Println("otherTask执行完成")
}

func syncService() chan string {
    
    
	// 声明一个channel,数据只能存放 string 类型
	resCh := make(chan string)

	// 创建一个协程去执行service任务
	go func() {
    
    
		ret := service()
		fmt.Println("service 结果已返回")
		// 因为不是用的 buffer channel,所以,协程会被阻塞在这一步的消息传递过程中,
		// 只有接受者拿到了 channel 中的消息,channel 放完消息后面的逻辑才会被执行。
		resCh <- ret // 存数据,从 channel 里面存放数据都用这个 “<-” 符号
		fmt.Println("channel 放完消息后面的逻辑")
	}()

	return resCh
}

// 异步返回执行结果,先调用 SyncService(),把它放入channel,用协程去执行,
// 然后主程序继续执行 otherTask(),最后把 SyncService() 的返回结果从 channel 里面取出来。
func TestSyncService(t *testing.T) {
    
    
	resCh := syncService()
	otherTask()
	fmt.Println(<-resCh) // 取数据,从 channel 里面存放数据都用这个 “<-” 符号
}

image-20230510145307325

It is optimized to 0.1sshow that otherTask()the execution time is 0.1 seconds, service()because it only takes 0.05 seconds, so the execution is completed ahead of time, and it is only necessary to fetch the results where needed, which greatly reduces the overall execution time of the program.

  • “<-”Use this symbol to store data from the channel
  • Declare the channel:make(chan string)

buffer channel returns asynchronously

We will find that there is still a small problem in the above mechanism, that is, after the service() is executed, put data into the channel, and the coroutine will be blocked here at this time, and the coroutine will wait until the receiver gets the message. Going forward, can we make the coroutine not block? When service() finishes executing, we put the message into the channel, and then continue to execute other logic. The answer is yes, at this point our buffer channel comes in handy.

func service() string {
    
    
	time.Sleep(time.Millisecond * 50)
	return "service执行完成"
}

func otherTask() {
    
    
	fmt.Println("otherTask的各种执行逻辑代码")
	time.Sleep(time.Millisecond * 100)
	fmt.Println("otherTask执行完成")
}

// 异步执行 service(), 并将结果放入 buffer channel
func syncServiceBufferChannel() chan string {
    
    
	// 声明一个 channel,数据只能存放 string 类型
	// 后面的数字表示 buffer 的容量
	resCh := make(chan string, 1)

	go func() {
    
    
		ret := service()
		fmt.Println("service 结果已返回")
		// 此时使用的是 buffer channel,所以只要 service() 结果返回了,buffer容量未满
		// channel放完消息后面的逻辑就会被执行,不会被阻塞。
		resCh <- ret // 存数据,从 channel 里面存放数据都用这个 “<-” 符号
		fmt.Println("channel 放完消息后面的逻辑")
	}()

	return resCh
}

// 异步返回执行结果,先调用 SyncService(),把它放入 buffer channel,用协程去执行,
// 此时协程不会被阻塞,然后主程序继续执行 otherTask(),
// 最后把 TestSyncServiceBufferChannel() 的返回结果从 channel 里面取出来。
func TestSyncServiceBufferChannel(t *testing.T) {
    
    
	resCh := syncServiceBufferChannel()
	otherTask()
	fmt.Println(<-resCh) // 取数据,从 channel 里面存放数据都用这个 “<-” 符号
}

image-20230510151432831

We will find that after the buffer channel is adopted, when the return result of service() is put into the buffer channel, the coroutine does not block, but continues to execute "the logic behind the channel after putting the message", and other results are consistent with the typical channel .

Although time is also the same 0.1s, we need to know that if there are many tasks and the execution time is long, the optimization must be very obvious.

Multiplex and timeout control

select multiplex mechanism

The syntax of select is very similar to the syntax of switch. Its execution order is not necessarily determined by the context of our code, but the result of which case is executed when the case is satisfied . If all channels are blocked, go to default.

select {
    
    
// 从 channel 上等待一个消息
case ret := <-retCh1:
	t.Logf("result:%s", ret)
// 从另一个 channel 上等待一个消息
case ret := <-retCh2:
	t.Logf("result:%s", ret)
// 如果所有的 channel 都处于阻塞中,则走 default
default:
	t.Error("No more returned")
}

timeout control

Using the multiplexing mechanism of select, we can implement a timeout mechanism. For example, when a channel has not returned a message for a long time, we will return a timeout.

select {
    
    
case ret := <-retCh1:
	t.Logf("result:%s", ret)
case ret := <-time.After(time.Second * 5):
	t.Error("time out")
}

time.After()After a period of time, its specific channel will return a message. When the set time is not reached, this case will be blocked here. When the duration we set is exceeded, this case can be obtained from the channel. A message, so it can be used for timeout control.

func service() string {
    
    
	time.Sleep(time.Millisecond * 50)
	return "service执行完成"
}

func otherTask() {
    
    
	fmt.Println("otherTask的各种执行逻辑代码")
	time.Sleep(time.Millisecond * 100)
	fmt.Println("otherTask执行完成")
}

func syncService() chan string {
    
    
	// 声明一个channel,数据只能存放 string 类型
	resCh := make(chan string)

	// 创建一个协程去执行service任务
	go func() {
    
    
		ret := service()
		fmt.Println("service 结果已返回")
		// 因为不是用的 buffer channel,所以,协程会被阻塞在这一步的消息传递过程中,
		// 只有接受者拿到了 channel 中的消息,channel 放完消息后面的逻辑才会被执行。
		resCh <- ret // 存数据,从 channel 里面存放数据都用这个 “<-” 符号
		fmt.Println("channel 放完消息后面的逻辑")
	}()

	return resCh
}

// 异步返回执行结果,先调用 SyncService(), 把它放入channel,用协程去执行,
// 然后主程序继续执行 otherTask(),最后把 SyncService() 的返回结果 从 channel 里面取出来。
func TestSyncService(t *testing.T) {
    
    
	select {
    
    
	case ret := <-syncService():
		otherTask()
		t.Logf("result:%s", ret)
	case <-time.After(time.Millisecond * 10):
		t.Error("time out")
	}
}

image-20230510154712189

Because service() needs to execute for 0.05 seconds, we set a timeout of 0.01 seconds, so time out is gone.

Channel closing and broadcasting

What happens if the channel is not closed

Write a program for a data producer and a data consumer. The data producer continuously produces data, and the consumer continuously consumes the data produced by the producer and interacts through the channel.

// 数据生产者
func dataProducer(ch chan int, wg *sync.WaitGroup) chan int {
    
    
	wg.Add(1)
	go func() {
    
    
		for i := 0; i < 10; i++ {
    
    
			ch <- i
		}
		wg.Done()
	}()
 
	return ch
}
 
// 数据消费者
func dataConsumer(ch chan int, wg *sync.WaitGroup) {
    
    
	wg.Add(1)
	go func() {
    
    
		for i := 0; i < 10; i++ {
    
    
			data := <-ch
			fmt.Println(data)
		}
		wg.Done()
	}()
}
 
// 数据消费者
func dataConsumer2(ch chan int, wg *sync.WaitGroup) {
    
    
	wg.Add(1)
	go func() {
    
    
		for i := 0; i < 10; i++ {
    
    
			data := <-ch
			fmt.Println(data)
		}
		wg.Done()
	}()
}
 
// channel还未关闭的场景
func TestChannelNotClosed(t *testing.T) {
    
    
	ch := make(chan int)
	var wg sync.WaitGroup
	dataProducer(ch, &wg)
	dataConsumer(ch, &wg)
	wg.Wait()
}

Once the data we produce is inconsistent with the data we consume, for example, the producer can generate 11 numbers, and the consumer still only consumes 10 numbers, or when the producer generates 10 numbers and the consumer consumes 11 numbers, it will Report the following error:

image-20230510194033431

In order to solve this problem, Go urgently needs the channel to have a closing function, and all subscribers will be broadcast after closing.

channel closing

image-20230510193339574

grammatical format

// 关闭 channel
close(channelName)
 
// ok=true表示正常接收,false表示通道关闭
if val, ok := <-ch; ok {
    
    
    // other code
}

When the channel is normally closed and the data receiver continues to receive data, the received data is the default value of the corresponding data of the channel.

// 数据生产者
func dataProducer(ch chan int, wg *sync.WaitGroup) chan int {
    
    
	wg.Add(1)
	go func() {
    
    
		for i := 0; i < 10; i++ {
    
    
			ch <- i
		}
		// 关闭 channel
		close(ch)
		//ch <- 11 // 向关闭的 channel 发送消息,会报 panic: send on closed channel
		wg.Done()
	}()

	return ch
}

// 数据消费者
func dataReceiver(ch chan int, wg *sync.WaitGroup) {
    
    
	wg.Add(1)
	go func() {
    
    
		// 我们这里多接收一个数据,看看拿到的值是什么
		for i := 0; i < 11; i++ {
    
    
			data := <-ch
			fmt.Print(data, " ")
		}
		wg.Done()
	}()
}

// 关闭channel
func TestCloseChannel(t *testing.T) {
    
    
	ch := make(chan int)
	var wg sync.WaitGroup
	dataProducer(ch, &wg)
	dataReceiver(ch, &wg)
	wg.Wait()
}

image-20230510195707682

We will find that when the channel is closed, we receive an extra value. Since the data type defined by our channel is int, the data type we get will be the default value of int type 0.

Generally, one of our channels may correspond to multiple consumers, so when the channel is closed, the broadcast mechanism will be often used to notify all consumers that the channel has been closed.

task cancellation

The traditional solution is, assuming that a task is being executed, we judge by setting the value of a variable in the shared memory trueto or . falseNow we will use CSP, select multiplexing mechanism and channel closing and broadcasting to implement the task cancellation function.

Realization principle

  • Broadcast a message on the channel through CSP to tell all coroutines that everyone can stop now.

how to judge

  • Through the select multiplexing mechanism, if a message is received from the channel, it means that the task cancellation function needs to be executed, otherwise it will not be executed.

code example

// 任务是否已被取消
// 实现原理:
// 检查是否从 channel 收到一个消息,如果收到一个消息,我们就返回 true,代表任务已经被取消了
// 当没有收到消息,channel 会被阻塞,多路选择机制就会走到 default 分支上去。
func isCanceled(cancelChan chan struct{
    
    }) bool {
    
    
	select {
    
    
	case <-cancelChan:
		return true
	default:
		return false
	}
}

// 执行任务取消
// 因为 close() 是一个广播机制,所以所有的协程都会收到消息
func execCancel(cancelChan chan struct{
    
    }) {
    
    
	// close(cancelChan)会使所有处于处于阻塞等待状态的消息接收者(<-cancelChan)收到消息
	close(cancelChan)
}

// 利用 CSP,多路选择机制和 channel 的关闭与广播实现任务取消功能
func TestCancel(t *testing.T) {
    
    
	var wg sync.WaitGroup
	cancelChan := make(chan struct{
    
    }, 0)

	// 启动 5 个协程
	for i := 0; i < 5; i++ {
    
    
		wg.Add(1)
		go func(i int, cancelChan chan struct{
    
    }, wg *sync.WaitGroup) {
    
    
			// 做一个 while(true) 的循环,一直检查任务是否有被取消
			for {
    
    
				if isCanceled(cancelChan) {
    
    
					fmt.Println(i, "is Canceled")
					wg.Done()
					break
				} else {
    
    
					// 其它正常业务逻辑
					time.Sleep(time.Millisecond * 5)
				}
			}
		}(i, cancelChan, &wg)
	}
	// 执行任务取消
	execCancel(cancelChan)
	wg.Wait()
}

image-20230510201314157

All coroutines are cancelled.

close()It is a broadcast mechanism that will make all message receivers in the blocking waiting state receive the message.

Context and task cancellation

Cancellation of associated tasks

Scenario : When we start multiple subtasks, subtasks and subtasks are associated:

image-20230510201502220

If we just want to cancel the task of a leaf node, it can be realized by using CSP, select multiplexing mechanism and channel closing and broadcasting.

image-20230510201653350

But our current scenario is that when we cancel the task of the parent node, we want to cancel all the tasks of the child node, how to achieve it?

image-20230510201742833

Of course, we can implement it ourselves, but since Golang 1.9, it has been Contextformally incorporated into Go's built-in package, and it is specially designed to do this.

Context

image-20230510201534133

ctx, cancel := context.WithCancel(context.Background())

context.WithCancel()method, context.Background()after the root node is passed in, one of the returned is ctx, and the other is the cancel method, calling the cancel method will execute the cancel function. And ctx can be passed to the subtask to cancel the subtask, so that both the parent node and the subtask are cancelled. The notification form of cancellation is to ctx.Done()get the message through to judge whether to receive the notification. This ctx.Done() is analogous to the close() in the channel, all channels will receive a notification.

Code

// 任务是否已被取消
// 实现原理:
// 通过 ctx.Done() 接收context的消息,如果收到消息,我们就返回 true,代表任务已经被取消了
// 当没有收到消息,多路选择机制就会走到 default 分支上去。
func isCanceled(ctx context.Context) bool {
    
    
	select {
    
    
	case <-ctx.Done():
		return true
	default:
		return false
	}
}

// 通过context实现任务取消功能
func TestCancel(t *testing.T) {
    
    
	var wg sync.WaitGroup
	// ctx传到子节点中去,可以取消子节点,调用cancel()方法则执行取消功能
	ctx, cancel := context.WithCancel(context.Background())

	// 启动 5 个协程
	for i := 0; i < 5; i++ {
    
    
		wg.Add(1)
		go func(i int, ctx context.Context, wg *sync.WaitGroup) {
    
    
			// 做一个 while(true) 的循环,一直检查任务是否有被取消
			for {
    
    
				if isCanceled(ctx) {
    
    
					fmt.Println(i, "is Canceled")
					wg.Done()
					break
				} else {
    
    
					// 其它正常业务逻辑
					time.Sleep(time.Millisecond * 5)
				}
			}
		}(i, ctx, &wg)
	}
	// 执行任务取消
	cancel()
	wg.Wait()
}

image-20230510203310557

The coroutines are all cancelled, as expected.

concurrent tasks

Execute only once - singleton pattern

  • Java code- singleton mode- lazy style- thread safety (double check)

image-20230515204029731

  • Go code

    sync.Once()It can ensure that the method inside Do()will only be executed once in the case of multi-threading.

    type Singleton struct {
          
          
    }
     
    var singleInstance *Singleton
    var once sync.Once
     
    // 获取一个单例对象
    func GetSingletonObj() *Singleton {
          
          
    	once.Do(func() {
          
          
    		fmt.Println("Create a singleton Obj")
    		singleInstance = new(Singleton)
    	})
     
    	return singleInstance
    }
     
    // 启动多个协程,测试我们单例对象是否只创建了一次
    func TestGetSingletonObj(t *testing.T)  {
          
          
    	var wg sync.WaitGroup
    	for i := 0; i < 5; i++ {
          
          
    		wg.Add(1)
    		go func() {
          
          
    			obj := GetSingletonObj()
    			fmt.Printf("%x\n", unsafe.Pointer(obj))
    			wg.Done()
    		}()
    	}
    	wg.Wait()
    }
    

    image-20230515205536923

    It can be seen that Do()the output content in the method is only printed once, and the address values ​​obtained by multiple coroutines are the same, realizing the singleton mode.

Just any task to complete

When we need to perform many concurrent tasks, but as long as any task is completed, the result can be returned to the user. For example, if we search for a certain search term from Baidu and Google at the same time, if any search engine returns first, we can return the results to the user, and we don’t need to return all scenarios.

  • Here we use the mechanism of CSP to realize this mode
// 从网站上执行搜索功能
func searchFromWebSite(webSite string) string {
    
    
	time.Sleep(10 * time.Millisecond)
	return fmt.Sprintf("search from %s", webSite)
}

// 收到第一个结果后立刻返回
func FirstResponse() string {
    
    
	var arr = [2]string{
    
    "baidu", "google"}
	// 防止协程泄露,这里用 buffer channel 很重要,否则可能导致剩下的协程会被阻塞在那里,
	// 当阻塞的协程达到一定量后,最终可能导致服务器资源耗尽而出现重大故障
	ch := make(chan string, len(arr))
	for _, val := range arr {
    
    
		go func(v string) {
    
    
			// 拿到所有结果放入 channel
			ch <- searchFromWebSite(v)
		}(val)
	}
	// 这里没有使用 WaitGroup,因为我们的需求是当 channel 收到第一个消息后就立刻返回
	return <-ch
}

func TestFirstResponse(t *testing.T) {
    
    
	t.Log("Before:", runtime.NumGoroutine()) // 输出当前系统中的协程数
	t.Log(FirstResponse())
	t.Log("After:", runtime.NumGoroutine()) // 输出当前系统中的协程数
}

image-20230516131515928

all tasks completed

Sometimes we need to complete all the tasks before proceeding to the next link. When we place an order successfully, only when the points and coupons are given will it show that all the discounts have been given successfully.

This mode can of course be implemented with WaitGroup, but we will use the CSP mechanism to implement it here.

// 送豪礼方法
func sendGift(gift string) string {
    
    
	time.Sleep(10 * time.Millisecond)
	return fmt.Sprintf("送%s", gift)
}

// 使用 CSP 拿到所有的结果才返回
func CspAllResponse() []string {
    
    
	var arr = [2]string{
    
    "优惠券", "积分"}
	// 防止协程泄露,这里用 buffer channel 很重要,否则可能导致剩下的协程会被阻塞在那里,
	// 当阻塞的协程达到一定量后,最终可能导致服务器资源耗尽而出现重大故障
	ch := make(chan string, len(arr))
	for _, val := range arr {
    
    
		go func(v string) {
    
    
			// 拿到所有结果放入 channel
			ch <- sendGift(v)
		}(val)
	}

	var finalRes = make([]string, len(arr), len(arr))
	// 等到所有的的协程都执行完毕,把结果一起返回
	for i := 0; i < len(arr); i++ {
    
    
		finalRes[i] = <-ch
	}
	return finalRes
}

func TestAllResponse(t *testing.T) {
    
    
	t.Log("Before:", runtime.NumGoroutine())
	t.Log(CspAllResponse())
	t.Log("After:", runtime.NumGoroutine())
}

image-20230516132812771

object pool

In our daily development, there are often database connections, network connections, etc., and we often need to pool them to prevent objects from being created repeatedly. In Go language, we can use buffered channel to implement object pool. By setting the size of buffer to set the size of the pool, we can get an object from this buffer pool and return it to the channel when it is used up.

// 可重用对象,比如连接等
type Reusable struct {
    
    
}

// 对象池
type ObjPool struct {
    
    
	bufChan chan *Reusable // 用于缓存可重用对象
}

// 创建一个包含多个可重用对象的对象池
func NewObjPool(numOfObj int) *ObjPool {
    
    
	// 声明对象池
	objPool := ObjPool{
    
    }
	// 初始化 objPool.bufChan 为一个 channel
	objPool.bufChan = make(chan *Reusable, numOfObj)
	// 往 objPool 对象池里面放多个可重用对象
	for i := 0; i < numOfObj; i++ {
    
    
		objPool.bufChan <- &Reusable{
    
    }
	}
	return &objPool
}

// 从对象池拿到一个对象
func (objPool *ObjPool) GetObj(timeout time.Duration) (*Reusable, error) {
    
    
	select {
    
    
	case ret := <-objPool.bufChan:
		return ret, nil
	case <-time.After(timeout): // 超时控制
		return nil, errors.New("time out")
	}
}

// 将可重用对象还回对象池
func (objPool *ObjPool) ReleaseObj(ReusableObj *Reusable) error {
    
    
	select {
    
    
	case objPool.bufChan <- ReusableObj:
		return nil
	default:
		return errors.New("overflow") // 超出可重用对象池容量
	}
}

// 从对象池里面拿出对象,用完了再放回去
func TestObjPool(t *testing.T) {
    
    
	pool := NewObjPool(3)
	for i := 0; i < 3; i++ {
    
    
		if obj, err := pool.GetObj(time.Second * 1); err != nil {
    
    
			t.Error(err)
		} else {
    
    
			fmt.Printf("%T\n", obj)
			if err := pool.ReleaseObj(obj); err != nil {
    
    
				t.Error(err)
			}
		}
	}
	t.Log("Done")
}

image-20230516134633855

sync.Pool object cache

In fact, sync.Pool is not an object pool class, but an object cache, called sync.Cache is more appropriate.

sync.Pool has two important concepts, private object and shared pool :

  • Private object : Coroutine safe, no lock required when writing.
  • Shared pool : Coroutines are not safe, and locks are required when writing.

Both of them are stored in the that we talked about earlier Processor.

sync.Pool object acquisition

image-20230516135420115

sync.Pool object put back

image-20230516135850416

sync.Pool Lifecycle

image-20230516140013906

This is why it cannot be used as an object pool.

Use sync.Pool

伪代码

// 使用 New 关键字创建新对象
pool := &sync.Pool{
    
    
	New: func() interface{
    
    } {
    
    
		return 0
	},
}
 
// 从 pool 中获取一个对象,因为返回的是空接口interface{},所以要自己做断言
array := pool.Get().(int)
 
// 往 pool 中放入一个对象
pool.Put(10)
basic use
// 调试 sync.Pool 对象
func TestSyncPool(t *testing.T) {
    
    
	pool := &sync.Pool{
    
    
		New: func() interface{
    
    } {
    
    
			fmt.Println("Create a new object")
			return 1
		},
	}

	// 第一次从池中获取对象,我们知道它一定是空的,所有肯定会调用 New 方法去创建一个新对象
	v := pool.Get().(int)
	fmt.Println(v) // 1

	// 放一个不存在的对象,它会优先放入私有对象
	pool.Put(2)
	// 此时私有对象已经存在了,所以会优先拿到私有对象的值
	v1 := pool.Get().(int)
	fmt.Println(v1) // 2

	// 模拟系统调用GC, GC会清除 sync.pool中缓存的对象
	//runtime.GC()
}

image-20230516145651073

A GC occurs during the process:

// 调试 sync.Pool 对象
func TestSyncPool2(t *testing.T) {
    
    
	pool := &sync.Pool{
    
    
		New: func() interface{
    
    } {
    
    
			fmt.Println("Create a new object")
			return 1
		},
	}

	// 第一次从池中获取对象,我们知道它一定是空的,所有肯定会调用 New 方法去创建一个新对象
	v := pool.Get().(int)
	fmt.Println(v) // 1

	// 放一个不存在的对象,它会优先放入私有对象
	pool.Put(2)
	// 模拟系统调用GC, GC会清除 sync.pool中缓存的对象
	runtime.GC()
	// 此时私有对象已经被GC掉了,所以这里又新建了一次对象
	v1 := pool.Get().(int)
	fmt.Println(v1) // 1
}

image-20230516150706206

The new object is created 2 times, as expected.

Note: The newly created object using Get()the method will not be placed in the private object, only Put()the method will be placed in the private object.

Application in multi-coroutine
// 调试 sync.Pool 在多个协程中的应用场景
func TestSyncPoolInMultiGoroutine(t *testing.T) {
    
    
	pool := sync.Pool{
    
    
		New: func() interface{
    
    } {
    
    
			fmt.Println("Create a new object")
			return 0
		},
	}

	pool.Put(1)
	pool.Put(2)
	pool.Put(3)

	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
    
    
		wg.Add(1)
		go func() {
    
    
			v, _ := pool.Get().(int)
			fmt.Println(v)
			wg.Done()
		}()
	}
	wg.Wait()
}

image-20230516151329903

sync.Pool summary

image-20230516151423032

unit test

Built-in unit testing framework

image-20230517101035997

func TestErrorInCode(t *testing.T) {
    
    
	fmt.Println("Start")
	t.Error("Error")
	fmt.Println("End")
}

func TestFailInCode(t *testing.T) {
    
    
	fmt.Println("Start")
	t.Fatal("Error")
	fmt.Println("End")
}

image-20230517101527274

With Errorthe test method, the test continues execution, with Fatalthe test method, the test is interrupted.

show code coverage

go test -v -cover

Affirmation

https://github.com/stretchr/testify

install assert:

go get -u github.com/stretchr/testify

image-20230517112432974

// 平方 故意+1计算错误,使断言生效
func square(num int) int {
    
    
	return num * num + 1
}
 
// 表格测试法
func TestSquare(t *testing.T) {
    
    
	// 输入值
	inputs := [...]int{
    
    1, 2, 3}
	// 期望值
	expected := [...]int{
    
    2, 4, 9}
 
	for i := 0; i< len(inputs); i++ {
    
    
		ret := square(inputs[i])
		// 调用 assert 断言包
		assert.Equal(t, expected[i], ret)
	}
}

image-20230517112848737

Benchmark

use

  • Conduct a performance evaluation of some code fragments in the program, and compare which writing method is better.
  • Conduct an evaluation of the third-party library to see which library performs better.

Example of use

image-20230517151428677

Use b.ResetTimer()and b.StopTimer()to isolate code not relevant to performance testing.

Code Test: Comparing the performance of string concatenation

// 通过“+=”的方式拼接字符串
func ConcatStringByLink() string {
    
    
	elements := [...]string{
    
    "1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
		"11", "12", "13", "14", "15", "16", "17", "18", "19", "20"}
	str := ""
	for _, elem := range elements {
    
    
		str += elem
	}
	return str
}

// 通过字节数组 bytes.buffer 拼接字符串
func ConcatStringByBytesBuffer() string {
    
    
	elements := [...]string{
    
    "1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
		"11", "12", "13", "14", "15", "16", "17", "18", "19", "20"}
	var buf bytes.Buffer
	for _, elem := range elements {
    
    
		buf.WriteString(elem)
	}
	return buf.String()
}

// 用benchmark测试字符串拼接方法的性能
func BenchmarkConcatStringWithLink(b *testing.B) {
    
    
	// 与性能测试无关的代码的开始位置
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
    
    
		ConcatStringByLink()
	}

	// 与性能测试无关代码的结束为止
	b.StopTimer()
}

// 用 benchmark 测试 bytes.buffer 连接字符串的性能
func BenchmarkConcatStringWithByteBuffer(b *testing.B) {
    
    
	// 与性能测试无关的代码的开始位置
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
    
    
		ConcatStringByBytesBuffer()
	}

	// 与性能测试无关代码的结束为止
	b.StopTimer()
}

Way code runs single run time
use +=stitching 1813815 649.9 ns/op
use bytes.Bufferstitching 6804018 172.6 ns/op

This is just 20 strings concatenated, the gap would be more noticeable if more strings were concatenated.

native command

// -bench= 后面跟方法名,如果是所有方法就写"."
go test -bench=.
 
// 注意:windows下使用 go test 命令时, -bench=.应该写成 -bench="."

// 如果想知道 代码每一次的内存分配情况,这种方案为什么快,那种方案为什么慢,可以加一个-benchmem参数
go test -bench=. -benchmem

image-20230517152647857

Through +=the way we use to allocsallocate space 19 times in total, but byte.Bufferonly once through the method, the performance improvement is here.

BDD

BDD (Behavior Driven Development), behavior-driven development.

In order to make the communication between us and our customers smoother, we will use the same "language" to describe a system to avoid the problem of inconsistencies in expression, and when there is any behavior, what will happen.

image-20230517154138740

image-20230517154428331

BDD in Go

goconvey project website:

https://github.com/smartystreets/goconvey/

Install
go get -u github.com/smartystreets/goconvey/convey
code example
package bdd

import (
	"testing"
	// 前面这个"."点,表示将import进来的package的方法是在当前名字空间的,可以直接使用里面的方法
	// 例如使用 So()方法,就可以直接用,不用写成 convey.So()
	. "github.com/smartystreets/goconvey/convey"
)

// BDD框架 convey的使用
func TestSpec(t *testing.T) {
    
    
	Convey("Given 2 even numbers", t, func() {
    
    
		a := 3
		b := 4

		Convey("When add the two numbers", func() {
    
    
			c := a + b
            
			Convey("Then the result is still even", func() {
    
    
				So(c%2, ShouldEqual, 0) // 判断c % 2是否为 0
			})
		})
	})
}

image-20230517161022762

Start the WEB UI
~/go/bin/goconvey 

image-20230517162927075

The web interface is very friendly:

image-20230517163014679

If there is a port conflict, you can solve it like this

~/go/bin/goconvey -port 8081

The notes are organized from the Geek Time video tutorial: Go language from entry to actual combat

Guess you like

Origin blog.csdn.net/weixin_53407527/article/details/130885859