Go Concurrency Visually Explained – Select statement

0 评论
/ /
300 阅读
/
11289 字
08 2023-08

Last week, I published a blog to visually explain how Channel in Golang works. If understanding Channel remains a difficulty for you, please check it out before coming back to this blog.

Go Concurrency Visually Explained — Channel
When it comes to concurrency, many programming languages adopt the Shared Memory/State Model. However, Go distinguishes…
medium.com

As a quick refresher: Partier, Candier, and Stringer run a cafe shop. Partier assists with accepting orders from customers and then passes those orders to the kitchen, where Candier and Stringer make coffee.

Gophers’ Cafe

In this blog, I will be visually explaining select statement, another great tool to handle concurrency in Go applications. Gophers and their imaginary cafe will still be my companions, but this time, let’s zoom in Partier and the ordering part.

Scenario

Gopher’s Cafe realizes that there are more and more customers want to order coffee online via Food Delivery apps. Hence, along with walk-in ordering, they also opt for a Food Delivery app. Partier watches for orders coming from both channels and forwards these orders to Candier and Stringer via another channel, name it queue.

select {
case order := <-appOrders:
  queue <- order
case order := <-inShopOrders:
  queue <- order
}

When there is an order coming to any of these 2 channels, Partier gets and forwards it to the queue.

If there are orders on both channels, one of them will be picked. In real cafes, orders from inShopOrders would probably be prioritized. However, in Go applications, we cannot guarantee which one will be picked. Also note that only one order is picked for one execution of select statement, Partier won’t pick one first then the other. Though, in many applications, select statement usually goes inside a for loop, making the leftover in the previous iteration a chance to be picked in the next iteration.

for {
  select {
  case order := <-appOrders:
    queue <- order
  case order := <-inShopOrders:
    queue <- order
  }
}

But, if there are orders on both channels, they will be subjected to a fair competition again.


Default

In the off-peak hours, not many orders coming, Partier spends a huge amount of time on waiting. He thinks that he can use time more productively by doing other things instead, e.g. cleaning the tables. This can be achieved with default.

for {
  select {
  case order := <-appOrders:
    log.Println("There is an order coming from appOrders channel")
    queue <- order
  case order := <-inShopOrders:
    log.Println("There is an order coming from inShopOrders channel")
    queue <- order
  default:
    log.Println("There is no order on both channels, I will do cleaning instead")
    doCleaning()
  }
}


time.After()

time.After(duration) is often used together with select to prevent waiting forever. Unlike default which is executed immediately when no channels are available, time.After(duration) creates an ordinary <-chan Time, waits for the duration to elapse and then sends the current time on the returned channel. This channel is treated equally among others in the select statement. As you can see, channels in a select statement can be of different types.

shouldClose := false
closeHourCh := time.After(8 * time.Hour)
for !shouldClose {
  select {
  case order := <-appOrders:
    log.Println("There is an order coming from appOrders channel")
    queue <- order
  case order := <-inShopOrders:
    log.Println("There is an order coming from inShopOrders channel")
    queue <- order
  case now := <-closeHourCh:
    log.Printf("It is %v now, the shop is closing\n", now)
    shouldClose = true
  default:
    log.Println("There is no order on both channels, I will go cleaning instead")
    doCleaning()
  }
}
log.Println("Shop is closed, I'm going home now. Bye!")


This technique is very common when dealing with remote API calls, as we cannot guarantee when the remote server returns or whether it does. Thank to context, we don’t usually need to do this.

responseChannel := make(chan interface{})
timer := time.NewTimer(timeout)
select {
case resp := <-responseChannel:
  log.Println("Processing response")
  processResponse(resp)
  timer.Stop()
case <-timer.C:
  log.Println("Time out, giving up")
}


Show me your code!

Let’s wrap-up this blog with a complete code of the imaginary cafe. One more thing to note here is that selecting from a closed channel will always return immediately. Therefore, if you find it necessary, use comma ok idiom. Getting your hands dirty is the best way of learning to code. Hence, if you are new with select , I suggest you copy and play around with this code on your IDE. Happy coding!

package main

import (
  "fmt"
  "log"
  "time"
)

func main() {
  appOrders := make(chan order, 3)
  inShopOrders := make(chan order, 3)
  queue := make(chan order, 3)

  go func() {
    for i := 0; i < 6; i++ {
      appOrders <- order(100 + i)
      time.Sleep(10 * time.Second)
    }
    close(appOrders)
  }()
  go func() {
    for i := 0; i < 4; i++ {
      inShopOrders <- order(200 + i)
      time.Sleep(15 * time.Second)
    }
    close(inShopOrders)
  }()
  go partier(appOrders, inShopOrders, queue)

  for o := range queue {
    log.Printf("Served %s\n", o)
  }
  log.Println("Done!")
}

func partier(
  appOrders <-chan order,
  inShopOrders <-chan order,
  queue chan<- order,
) {
  shouldClose := false
  closeTimeCh := time.After(1 * time.Minute)
  for !shouldClose {
    select {
    case ord, ok := <-appOrders:
      if ok {
        log.Printf("There is %s coming from appOrders channel\n", ord)
        queue <- ord
      }
    case ord, ok := <-inShopOrders:
      if ok {
        log.Printf("There is %s coming from inShopOrders channel\n", ord)
        queue <- ord
      }
    case now := <-closeTimeCh:
      log.Printf("It is %v now, the shop is closing\n", now)
      shouldClose = true
    default:
      log.Println("There is no order on both channels, I will go cleaning instead")
      doCleaning()
    }
  }
  close(queue)
  log.Println("Shop is closed, I’m going home now. Bye!")
}

func doCleaning() {
  time.Sleep(5 * time.Second)
  log.Println("Partier: Cleaning done")
}

type order int

func (o order) String() string {
  return fmt.Sprintf("order-%02d", o)
}


If you find this article helpful, please motivate me with one clap. You can also check out my other articles at https://medium.com/@briannqc and connect to me on LinkedIn. Thanks a lot for reading!

Thank you for reading until the end. Please consider following the writer and this publication. Visit Stackademic to find out more about how we are democratizing free programming education around the world.