Thoughts on Go select deadlock

Posted by Mick33520 on Mon, 07 Mar 2022 08:50:37 +0100

Thoughts on Go select deadlock

https://mp.weixin.qq.com/s/Ov1FvLsLfSaY8GNzfjfMbg Continuous thinking triggered by the article

Summary above

Summary I

package main

import (
 "fmt"
)

func main() {
 ch := make(chan int)
 go func() {
  select {
  case ch <- getVal(1):
   fmt.Println("in first case")
  case ch <- getVal(2):
   fmt.Println("in second case")
  default:
   fmt.Println("default")
  }
 }()

 fmt.Println("The val:", <-ch)
}

func getVal(i int) int {
 fmt.Println("getVal, i=", i)
 return i
}

No matter which case is finally selected by select, getVal() will execute in the order of source code: getVal(1) and getVal(2), that is, they must output first:

getVal, i= 1
getVal, i= 2

Summary II

package main

import (
 "fmt"
 "time"
)

func talk(msg string, sleep int) <-chan string {
 ch := make(chan string)
 go func() {
  for i := 0; i < 5; i++ {
   ch <- fmt.Sprintf("%s %d", msg, i)
   time.Sleep(time.Duration(sleep) * time.Millisecond)
  }
 }()
 return ch
}

func fanIn(input1, input2 <-chan string) <-chan string {
 ch := make(chan string)
 go func() {
  for {
   select {
   case ch <- <-input1:
   case ch <- <-input2:
   }
  }
 }()
 return ch
}

func main() {
 ch := fanIn(talk("A", 10), talk("B", 1000))
 for i := 0; i < 10; i++ {
  fmt.Printf("%q\n", <-ch)
 }
}

Each time you enter the following select statement:

select {
case ch <- <-input1:
case ch <- <-input2:
}

< - input1 and < - input2 will be executed, and the corresponding values are: A x and B x (where x is 0-5). However, each select will only select one of the case s for execution, so one of the results of < - input1 and < - input2 must be discarded, that is, it will not be written into ch. Therefore, a total of only 5 times will be output, and the other 5 times will be lost. (you will find that among the five output results, X is 0 1 2 3 4, for example)

In main, there are 10 cycles and only 5 results are obtained, so a deadlock is reported after 5 times of output.

If you change to this, everything is normal:

select {
case t := <-input1:
  ch <- t
case t := <-input2:
  ch <- t
}

My understanding:
Case ch < - < - input: the statement is executed in two sections, which can be understood as

t := <- input //When the case selection is not clear, it will be executed
ch <- t //If this case is not selected, this statement is not executed
 And these are two statements, in order
 therefore<-input This is not selected after execution caseļ¼Œ<-input The result will be discarded, resulting in the above deadlock problem.

Extension of the problem

Mentioned above
No matter which case is finally selected by select, getVal() will execute in the order of source code: getVal(1) and getVal(2), that is, they must output first:

getVal, i= 1
getVal, i= 2

Think 1: if the execution time of getVal() method is different, does the running time of select depend on the running time or the sum of the running time?

func getVal1(i int) int {
	time.Sleep(time.Second * 1)
	fmt.Println("getVal, i=", i)
	return i
}
func getVal2(i int) int {
	time.Sleep(time.Second * 2)
	fmt.Println("getVal, i=", i)
	return i
}

func main() {
	ch := make(chan int)
	go func() {
		for {
			beginTime := time.Now()
			select {
			case ch <- getVal1(1):
			case ch <- getVal2(2):
			default:
			     fmt.Println("")
			}
			fmt.Println(time.Since(beginTime))
		}
	}()
	time.Sleep(time.Second * 10)
}

Output results

getVal, i= 1
getVal, i= 2
3.0015862s
getVal, i= 1
getVal, i= 2
3.0021938s
getVal, i= 1
getVal, i= 2
3.0019246s

It can be seen that each select will execute the case statements in order, and the execution time of the select is the sum of the case statements
Of course, there will be no such writing in actual production
Correct writing:

func main() {
	begin := time.Now()
	ch := make(chan int)
	ch2 := make(chan int, 2)
	go func() {
		ch2 <- getVal1(1)
	}()
	go func() {
		ch2 <- getVal2(2)
	}()
	go func() {
		for {
			select {
			case d := <-ch2:
				ch <- d
			}
		}
	}()
	for i := 0; i < 2; i++ {
		fmt.Println(<-ch)
	}
	fmt.Println(time.Since(begin))
}

The output result depends on the longest running getVal()

getVal, i= 1
1
getVal, i= 2
2
2.0020979s

In actual production, the select statement is only used to accept the value in the channel, not to execute a method

Careful friends have found that there are two bug s in the above writing

  1. In a new start-up process, the for statement causes it to idle all the time, and the process will not be destroyed
  2. If you send data to ch after it is close d, panic will result

Add some comments to see the output

func main() {
	begin := time.Now()
	ch := make(chan int)
	ch2 := make(chan int, 2)
	go func() {
		ch2 <- getVal1(1)
	}()
	go func() {
		ch2 <- getVal2(2)
	}()
	time.Sleep(2 * time.Second)
	fmt.Println("goroutine num", runtime.NumGoroutine())
	go func() {
		defer func() {
			if r := recover(); r != nil {
				fmt.Println("panic err", r)
			}
		}()
		for {
			select {
			case d := <-ch2:
				ch <- d
			}
		}
	}()
	for i := 0; i < 2; i++ {
		fmt.Println(<-ch)
	}
	close(ch)
	fmt.Println(time.Since(begin))
	fmt.Println("goroutine num", runtime.NumGoroutine())
	ch2 <- 1
	time.Sleep(time.Second * 1)
}

Output results

getVal, i= 1
getVal, i= 2
goroutine num 2
1
2
2.0020965s
goroutine num 2
panic err send on closed channel

It can be seen that the coroutine of the for loop is not released, and a panic exception is also reported in the subsequent ch < - operation

Topics: Go