Do you know several Go concurrency control methods

Posted by musicbase on Tue, 08 Feb 2022 08:42:23 +0100

introduction

In Golang, a goroutine can be opened through the go keyword. Therefore, it is easy to write concurrent code in go. However, how to effectively control these growines executed concurrently?

When it comes to concurrency control, many people may first think of locks. Golang also provides lock related mechanisms, including mutex sync Mutex, and read / write lock sync RWMutex. In addition to locks, there are atomic operations such as sync/atomic. However, these mechanisms focus on the concurrent data security of goroutines. What this article wants to discuss is the concurrency behavior control of goroutine.

In goroutine concurrency behavior control, there are three common methods: WaitGroup, channel and Context.

WaitGroup

WaitGroup is located under sync package. Its usage is as follows.

func main() {
  var wg sync.WaitGroup

  wg.Add(2) //Add workload to be completed 2

  go func() {
    wg.Done() //Completed workload 1
    fmt.Println("goroutine 1 finish the work!")
  }()

  go func() {
    wg.Done() //Completed workload 1
    fmt.Println("goroutine 2 finish the work!")
  }()

  wg.Wait() //Wait for workload 2 to be completed
  fmt.Println("be-all goroutine Work has been completed!")
}

output:
//goroutine 2 finish the work!
//goroutine 1 finish the work!
//All goroutine s have finished their work!

WaitGroup's concurrency control method is especially suitable for: a task requires multiple goroutines to work together. Each goroutine can only do part of the task. Only when all goroutines are completed can the task be considered completed. Therefore, WaitGroup, like the meaning of its name, is a way of waiting.

However, in the actual business, there is such a scenario: when a certain requirement is met, it is necessary to actively notify a goroutine to end. For example, we start a background monitoring goroutine. When monitoring is no longer needed, we should notify the monitoring goroutine to end, otherwise it will idle all the time and cause leakage.

Channel

There is nothing WaitGroup can do about the above scenario. The simplest way you can think of: define a global variable and notify by modifying it elsewhere. The background goroutine will keep checking the variable. If it is found that the variable has changed, it will close itself, but this method is a little clumsy. In this case, channel+select can be used.

func main() {
  exit := make(chan bool)

  go func() {
    for {
      select {
      case <-exit:
        fmt.Println("Exit monitoring")
        return
      default:
        fmt.Println("Monitoring")
        time.Sleep(2 * time.Second)
      }
    }
  }()

  time.Sleep(5 * time.Second)
  fmt.Println("Notify monitoring exit")
  exit <- true

  //Prevent main goroutine from exiting prematurely
  time.Sleep(5 * time.Second)
}

Output:
//Monitoring
//Monitoring
//Monitoring
//Notify monitoring exit
//Exit monitoring

This combination of channel+select is an elegant way to notify goroutine of the end.

However, the scheme also has limitations. Imagine, what if there are multiple goroutines that need to control the end? What if these goroutines lead to other goroutines? Of course, we can define many channel s to solve this problem, but the relationship chain of goroutine leads to the complexity of this scenario.

Context

The above scenarios are common in the CS architecture model. In Go, A separate goroutine (A) is often opened for each client to process its series of requests, and often A single A will also request other services (start another goroutine B), B may also request another goroutine C, and C will send the request to A server such as Databse. Imagine that when the client is disconnected, the associated A, B and C need to exit immediately before the system can recover the resources occupied by A, B and C. Quitting A is simple, but how to inform B and C to quit?

At this time, Context appears.

func A(ctx context.Context, name string)  {
  go B(ctx ,name) //A called B
  for {
    select {
    case <-ctx.Done():
      fmt.Println(name, "A sign out")
      return
    default:
      fmt.Println(name, "A do something")
      time.Sleep(2 * time.Second)
    }
  }
}

func B(ctx context.Context, name string)  {
  for {
    select {
    case <-ctx.Done():
      fmt.Println(name, "B sign out")
      return
    default:
      fmt.Println(name, "B do something")
      time.Sleep(2 * time.Second)
    }
  }
}

func main() {
  ctx, cancel := context.WithCancel(context.Background())

  go A(ctx, "[[request 1]") //1 connection request from simulated client

  time.Sleep(3 * time.Second)
  fmt.Println("client Disconnect and notify the corresponding processing client Requested A,B sign out")
  cancel() //Assuming that a certain condition is met and the client is disconnected, the cancellation signal is propagated, CTX Get cancel signal in done()

  time.Sleep(3 * time.Second)
}

Output:
//[request 1] A do something
//[request 1] B do something
//[request 1] A do something
//[request 1] B do something
//The client disconnects and notifies a and B that handle the client request to exit
//[request 1] B exit
//[request 1] A exit

In the example, the connection request from the client is simulated. Accordingly, Goroutine A is started for processing, and a starts B processing at the same time. Both a and B use Context for tracking. When we use the cancel function to notify cancellation, these two goroutines will be ended.

This is the control ability of Context. It is like a controller. After pressing the switch, all sub contexts based on this Context or derived from it will be notified. At this time, you can clean up and finally release goroutine, which gracefully solves the uncontrollable problem after goroutine is started.

The detailed usage of Context is beyond the scope of this article. There will be a special explanation article on the Context package later. Please pay attention.

summary

This paper lists three concurrent behavior control modes in Golang. There is no difference between good and bad modes, only that different scenarios use appropriate schemes. In actual projects, it is often used in a variety of ways.

  • WaitGroup: the task processing of multiple goroutine s has dependency or splicing relationship.
  • channel+select: you can actively cancel goroutine; Data transmission in multiple groutine; channel can replace the work of WaitGroup, but it will increase the complexity of code logic; Multiple channels can meet the function of Context. Similarly, it will also complicate the code logic.
  • Context: signal propagation between multiple levels of growines (including metadata propagation, cancellation of signal propagation, timeout control, etc.).

Topics: Go