In the previous two articles, WE covered Goroutine and Channel in detail, which are the foundation of concurrent programming for Go. Today’s article introduces another important role in Go concurrent programming — multiplexing.

1. Why is multiplexing needed

The Go program creates a Goroutine for each task when it concurrently processes several tasks, and then needs to do different processing based on the results returned by different Goroutines.

If we get the result for each channel separately as usual:

taskCh1 := make(chan bool)
taskCh2 := make(chan bool)
taskCh3 := make(chan bool)

go run(taskCh1)
go run(taskCh2)
go run(taskCh3)

for {
    // Receive the result of channel 1
    result1 := <-taskCh1
    // Receive the result of channel 2
    result2 := <-taskCh2
    // Receive the result of channel 3
    result3 := <-taskCh3
}
Copy the code

The problem with this code is that it waits for all of the goroutines to complete. This implementation is inefficient because every channel value is blocked.

When dealing with multiple channels, it becomes difficult to receive data from multiple channels simultaneously.

And in some cases, you need to do something different depending on which channel is returned first, which is not possible in this way, so you need to use multiplexing.

Go provides a select mechanism to solve this problem.

2. Select Basic use

The select syntax is similar to that of switch, which takes a variable and does different things to it depending on the value of the variable. The select operation takes a channel operation:

ch := make(chan int.1) // In this case, the buffer channel must be used

for {
	select {
	case <-ch:
		time.Sleep(time.Second)
		fmt.Println("case 1 invoke")
	case data := <-ch:
		time.Sleep(time.Second)
		fmt.Printf("case 2 invoke %d\n", data)
	case ch <- 100:
		time.Sleep(time.Second)
		fmt.Println("case3 invoke")}Copy the code

In a select case, there are three operations that can be performed:

  1. < -ch: receives the channel, but does not process the value
  2. Data := <-ch: receives the channel and processes the results from the channel
  3. Ch < -100: data is sent to the channel

Once the above program is running, case 3 will be executed first, then casE1 and case2 will be executed at random, and the program will keep alternating.

If you modify the code in the first example above with select, it looks like this:

for {
		select {
		// Receive the result of channel 1
		case r := <-taskCh1:
			fmt.Printf("task1 result %+v\n", r)
		// Receive the result of channel 2
		case r := <-taskCh2:
			fmt.Printf("task2 result %+v\n", r)
		// Receive the result of channel 3
		case r := <-taskCh3:
			fmt.Printf("task3 result %+v\n", r)
		}
}
Copy the code

Select responds to every ready channel in a timely manner, whether sending or receiving data.

3. Handle timeouts

In addition to processing multiple channels at the same time, select can also be used to handle some cases of channel timeout. If there is no external interference, the channel will wait for a long time, but you can use select timeout to break the block:

ch := make(chan int.1)

select {
case data := <- ch:
	fmt.Printf("case invoke %+v\n", data)
case <-time.After(3 * time.Second):
	fmt.Println("channel timeout")}Copy the code

The above code creates a channel, but does not send data to the channel. If you do not use select, the program will be deadlocked.

Two cases were added to select. One case fetched data from the channel, but it certainly didn’t, so after 3 seconds, the other case will execute, returning a channel timeout message, thus avoiding the application waiting forever.

Another case is that we sometimes need to use the keyboard to obtain other input devices to send signals to the program, can also be achieved by this way, the above program is modified again:

ch := make(chan int.1)

quitCh := make(chan bool.1)

go func(ch chan bool) {
	var quit string
	fmt.Printf("quit? are you sure?: ")
	fmt.Scanln(&quit)
	quitCh <- true
}(quitCh)

select {
case data := <- ch:
	fmt.Printf("case invoke %+v\n", data)
case <-quitCh:
	fmt.Println("program quit")}Copy the code

This time no longer through the timeout to control, but through the keyboard to control, a new channel, only after the keyboard input, will send data to the channel, so that you can do free control program exit.

4. Non-blocking SELECT

In the above sample code, there is actually a part missing, look at the following code:

ch := make(chan int)

for {
	select {
	case <-ch:
		fmt.Println("case invoke")}}Copy the code

Because the select has only one case, and that case will never receive data, so the select itself is blocked, and the program can’t continue, causing a deadlock. In this case, we set an available case, This problem can be solved by making select non-blocking.

ch := make(chan int)

for {
	select {
	case <-ch:
		fmt.Println("case invoke")
	default:
		time.Sleep(time.Second)
		fmt.Println("default invoke")}}Copy the code

In this way, the program does not deadlock, but continuously executes the content in default.

5. Summary

In this article, we introduce channel multiplexing and illustrate scenarios where multiplexing can be used. In the next article, we’ll take a closer look at how Go implements the traditional concurrency model.

The text/Rayjun