Example of a non blocking read and write method for the Golang channel

  • 2020-06-19 10:28:32
  • OfStack

Whether it's a non-buffered channel or a buffered channel, there are some cases where we don't want to read or write data to be blocked, and there is only one solution, and that is to use the select structure.

This article shows you which situations can be blocked and how to use select to resolve the blocking.

Blocking the scene

There are altogether 4 blocking scenarios, 2 with cache and 2 without buffer.

The feature of the bufferless channel is that the sent data needs to be read before the sending can be completed. It blocks the scenario:

There is no data in a channel, but a read channel is executed. There is no data in the channel. It writes data to the channel, but no coroutine reads it.

//  scenario 1
func ReadNoDataFromNoBufCh() {
 noBufCh := make(chan int)

 <-noBufCh
 fmt.Println("read from no buffer channel success")

 // Output:
 // fatal error: all goroutines are asleep - deadlock!
}

//  scenario 2
func WriteNoBufCh() {
 ch := make(chan int)

 ch <- 1
 fmt.Println("write success no block")
 
 // Output:
 // fatal error: all goroutines are asleep - deadlock!
}

Note: The Output annotation in the sample code represents the result of the function execution, with each function unable to proceed due to blocking in the channel operation, resulting in a deadlock error.

The feature of cache channel is that when there is cache, the data can be written into the channel and returned directly; when there is data in the cache, the data can be read from the channel and returned directly. In this case, the cache channel will not block, and the blocking scenario is as follows:

The channel caches no data, but executes a read channel. The channel cache is full and writes to the channel, but no coroutine reads.

//  scenario 1
func ReadNoDataFromBufCh() {
 bufCh := make(chan int, 1)

 <-bufCh
 fmt.Println("read from no buffer channel success")

 // Output:
 // fatal error: all goroutines are asleep - deadlock!
}

//  scenario 2
func WriteBufChButFull() {
 ch := make(chan int, 1)
 // make ch full
 ch <- 100

 ch <- 1
 fmt.Println("write success no block")
 
 // Output:
 // fatal error: all goroutines are asleep - deadlock!
}

Select is used to achieve non-blocking reads and writes

select is a structure that performs a selection operation, it has a set of case statements, it executes the non-blocking one, if all of them are blocked, it waits for one of them not to block, and then it continues, it has one default statement, it never blocks, we can use it to achieve non-blocking operations.

The following example code USES select's modified unbuffered channel and buffered channel reads and writes. The following function can be called directly by the main function. The Ouput comment is the result of the run.


//  Unbuffered channel read 
func ReadNoDataFromNoBufChWithSelect() {
 bufCh := make(chan int)

 if v, err := ReadWithSelect(bufCh); err != nil {
  fmt.Println(err)
 } else {
  fmt.Printf("read: %d\n", v)
 }

 // Output:
 // channel has no data
}

//  Buffer channel read 
func ReadNoDataFromBufChWithSelect() {
 bufCh := make(chan int, 1)

 if v, err := ReadWithSelect(bufCh); err != nil {
  fmt.Println(err)
 } else {
  fmt.Printf("read: %d\n", v)
 }

 // Output:
 // channel has no data
}

// select The structure implements channel reading 
func ReadWithSelect(ch chan int) (x int, err error) {
 select {
 case x = <-ch:
  return x, nil
 default:
  return 0, errors.New("channel has no data")
 }
}

//  Unbuffered channel write 
func WriteNoBufChWithSelect() {
 ch := make(chan int)
 if err := WriteChWithSelect(ch); err != nil {
  fmt.Println(err)
 } else {
  fmt.Println("write success")
 }

 // Output:
 // channel blocked, can not write
}

//  Buffer channel write 
func WriteBufChButFullWithSelect() {
 ch := make(chan int, 1)
 // make ch full
 ch <- 100
 if err := WriteChWithSelect(ch); err != nil {
  fmt.Println(err)
 } else {
  fmt.Println("write success")
 }

 // Output:
 // channel blocked, can not write
}

// select The structure implements channel writes 
func WriteChWithSelect(ch chan int) error {
 select {
 case ch <- 1:
  return nil
 default:
  return errors.New("channel blocked, can not write")
 }
}

Use Select+ timeout to improve non-blocking reads and writes

Non-blocking channel blocking using default has one drawback: it returns when the channel is not read or written. In a real world scenario, more often than not, we want to try to read 1 to read data, or try to write 1 to read data, and if it's really hard to read or write data, come back, and the program goes on doing something else.

Using timers instead of default can solve this problem. For example, the tolerance time I give to the channel to read and write data is 500ms, if it is still unable to read and write, it will immediately return, change 1 will be like this:


func ReadWithSelect(ch chan int) (x int, err error) {
 timeout := time.NewTimer(time.Microsecond * 500)

 select {
 case x = <-ch:
  return x, nil
 case <-timeout.C:
  return 0, errors.New("read time out")
 }
}

func WriteChWithSelect(ch chan int) error {
 timeout := time.NewTimer(time.Microsecond * 500)

 select {
 case ch <- 1:
  return nil
 case <-timeout.C:
  return errors.New("write time out")
 }
}

The result becomes a timeout return:

[

read time out
write time out
read time out
write time out

]

Related articles: