// It's a restaurant simulation with customers placing orders and cooks // preparing meals. The waiter can hold 3 orders at a time, and customers // will wait for 5 seconds before abandoning an order if the waiter is busy. // Each of the 10 customers will eat 5 meals before leaving the restaurant. // Oh, and the three cooks, Remy, Colette, and Linguini, actually will deliver // the meals directly to the customers. package main import ( "log" "math/rand" "sync" "sync/atomic" "time" ) // A little utility that simulates performing a task for a random duration. // For example, calling do(10, "Remy", "is cooking") will compute a random // number of milliseconds between 5000 and 10000, log "Remy is cooking", // and sleep the current goroutine for that much time. func do(seconds int, action ...any) { log.Println(action...) randomMillis := 500 * seconds + rand.Intn(500 * seconds) time.Sleep(time.Duration(randomMillis) * time.Millisecond) } // An order for a meal is placed by a customer and is taken by a cook. // When the meal is finished, the cook will send the finished meal through // the reply channel. Each order has a unique id, safely incremented using // an atomic counter. It turns out the atomic counter is necessary here // because the counter is updated by multiple goroutines. type Order struct { id uint64 customer string reply chan *Order preparedBy string } var nextId atomic.Uint64 // The waiter is represented by a channel of orders. The waiter will // take orders from customers and send them to the cook. The cook will // then send the prepared meal back to the waiter. To simulate a waiter // being busy, the waiter channel has a buffer of 3 orders. var Waiter = make(chan *Order, 3) // A cook spends their time fetching orders from the order channel, // cooking the requested meal, and sending the meal back through the // order's reply channel. func Cook(name string) { log.Println(name, "starting work") for order := range Waiter { do(10, name, "cooking order", order.id, "for", order.customer) order.preparedBy = name order.reply <- order } } // A customer eats five meals and then goes home. Each time they enter the // restaurant, they place an order with the waiter. If the waiter is too // busy, the customer will wait for 5 seconds before abandoning the order. // If the order does get placed, then they will wait as long as necessary // for the meal to be cooked and delivered. func Customer(name string, wg *sync.WaitGroup) { defer wg.Done() ch := make(chan *Order) for mealsEaten := 0; mealsEaten < 5; { order := &Order{id: nextId.Add(1), customer: name, reply: ch} log.Println(name, "placed order", order.id) select { case Waiter <- order: meal := <-ch do(2, name, "eating cooked order", meal.id, "prepared by", meal.preparedBy) mealsEaten += 1 case <-time.After(7 * time.Second): do(5, name, "waiting too long, abandoning order", order.id) } } log.Println(name, "going home") } func main() { var customersDone sync.WaitGroup customers := []string{ "Ani", "Bai", "Cat", "Dao", "Eve", "Fay", "Gus", "Hua", "Iza", "Jai", } for _, customer := range customers { customersDone.Add(1) go Customer(customer, &customersDone) } go Cook("Remy") go Cook("Colette") go Cook("Linguini") customersDone.Wait() close(Waiter) log.Println("Restaurant closing") }
Here is an ungolfed version:
Fd}1TFN}1Tp%"%4d"*Nd)k
Answer: Concurrency is about managing independent tasks that may or may not run simultaneously, while parallelism specifically refers to truly simultaneous execution of tasks (on multicore or multicomputer systems).
Answer: A thread is an actual object, while a task is a more abstract “unit of work” or the code itself that runs concurrently with other tasks.
Answer: Java: The method is called. Ada: A Tasking_Error exception is raised.
Answer: Ada: program terminates only when the main task and all its dependents finish, so Ada waits for other tasks. Java: the program terminates when the main thread and all non-daemon threads finish, so it waits for some but not all. Go: program terminates when main goroutine finishes, without waiting for any other goroutines.
Answer: An unbuffered channel has no actual capacity and is fully synchronous, meaning each side blocks for the other to be ready. A buffered channel has a capacity blocking only when the buffer is full (for sending) or empty (for receiving). An example use case of an unbuffered channel is to make a simple latch. An example use case for a buffered channel is an event queue.
RWMutex) in Go. When would you choose one over the other?
Answer: A plain mutex does not allow any concurrent access, while a read-write mutex allows multiple readers or one writer at a time (concurrent reads but exclusive writes). Choose a plain mutex for simple operations or when there are a lot of writes. Choose a read-write when there are many reads and few writes.
Answer: Reading from a closed channel will read any queued values, but if there are none, will return the zero value immediately without blocking. Writing to a closed channel causes a panic. You can detect if a channel is closed by using the two-value assignment: value, ok := <-ch`, where ok is false if the channel is closed.
select statement in Go and how it differs from a switch statement. What happens when multiple channels in a select are ready simultaneously?
Answer: The select statement in Go waits on several channel operations and runs a case that is ready. A switch statement in many programming languages chooses what to do next based on arbitrary boolean expressions (rather than channel readiness). If more than one channel in a select is ready simultaneously, Go chooses one of the ready cases at random.