神刀安全网

Silly mistakes can make for great interview questions

Recently I’ve been writing more and more Go out of the need to create software that is both concurrent and parallel .

However, Go is no panacea; even the strongest synchronization primitives cannot save you from your own mistakes. In the time that I spent debugging subtle deadlocks and race conditions in my code I realized that it is the very subtlety of these errors that can make for a great interview question; the kind of question that accurately represents the day to day of the role that you’re most likely interviewing for.

I want to share two mistakes that I made in the last week (which I have rid of unnecessary complexity for the purpose of this post) as examples of two possible interview questions.

Example 1

My first example is a race condition in which I create and then wait on a set of concurrent goroutines (light weight threads in Go) to run to completion.

In the code below I first create a Wait Group; a synchronization primitive in which you can wait on the completion of other goroutines, akin to Thread.yield in Java.

I then start 10 goroutines which for the purpose of this example do nothing but increment a counter in the WaitGroup via wg.Add(1) and then decrement it via a deferred call to wg.Done() to signal their completion.

This is quite a simple example so you may have found the bug already. If not, take some time to think about the non-deterministic order of execution of this code. If you still haven’t found the bug read past the end of this figure for the answer.

wg := sync.WaitGroup{}  for i := 0; i < 10; i++ {     go func(i int) {         wg.Add(1)         defer wg.Done()         // Do something here     }(i) }  wg.Wait()

Answer

In the example above I create 10 goroutines. Each goroutine increments a counter in the Wait Group, does some arbitrary processing and then decrements the counter to signal that it has completed.

However, the Go runtime has to first create and then schedule each of these goroutines. The runtime makes no guarantee that all calls to wg.Add(1) are invoked before the call to wg.Wait() . Therefore, a typical ordering of events is that wg.Done() is called before even a single goroutine has incremented the wait counter. In such cases the program terminates before any goroutine is even scheduled, let alone run to completion.

The mistake in this example is that I had called wg.Add(1) inside the goroutine when in fact I should invoke it outside and prior to creating each goroutine. By calling wg.Add(1) in the main goroutine I can the guarantee that all invocations of wg.Add(1) complete before the call to wg.Wait() at the end of the program.

wg := sync.WaitGroup{}  for i := 0; i < 10; i++ {     wg.Add(1)     go func(i int) {         defer wg.Done()         // Do something here     }(i) }  wg.Wait()

Example 2

My second example extends the previous example, however this time I unintentionally create a deadlock. Unlike the previous example, I find that I now need to send a result from each of the goroutines created inside the loop back to the main goroutine. The pragmatic way to do this in Go is with a channel .

Having initialized a channel c I send the value of i into the channel from every goroutine created inside the loop. The main goroutine will later received each value of i in a non-deterministic order via the range over c . However, the following code results in a deadlock. The program never terminates.

c := make(chan int) wg := sync.WaitGroup  for i := 0; i < 10; i++ {     wg.Add(1)     go func(i int) {         c <- i         wg.Done()     }(i) }  wg.Wait() close(c)  for i := range(c) {     // Do something with c }

Answer

In Go there are two types of channel : buffered channels and unbuffered channels. Whilst a buffered channel behaves like a thread safe FIFO, unbuffered channels combine message passing with synchronization. In other words, a goroutine can only write into an unbuffered channel provided there is a reader in another goroutine also trying to read from the channel.

However, if you look in the example above I’ve created an unbuffered channel and not a buffered channel. Therefore, each of the goroutines created inside the loop attempts to write into the channel and subsequently blocks because there is no reader currently trying to read from the channel. Meanwhile, the main goroutine has invoked wg.Wait() and is currently blocked awaiting all other goroutines to complete. Comically, these goroutines cannot complete until the main goroutine unblocks the channel for each goroutine that is attempting to write into the channel. The result is a chicken and egg problem in which no progress can be made.

The mistake therefore is that I had created an unbuffered channel when I had actually intended to create a buffered channel.

c := make(chan int, 10) wg := sync.WaitGroup  for i := 0; i < 10; i++ {     wg.Add(1)     go func(i int) {         c <- i         wg.Done()     }(i) }  wg.Wait() close(c)  for i := range(c) {     // Do something with c }

Closing remarks

The examples that I have shown above are most likely too simple for the majority of candidates. In practice code is typically much more complex with many more moving parts. However, I hope that these examples have helped add some validity to my initial observation. Everyone makes mistakes. The perfect candidate is not one that never makes mistakes but is one that can clearly and effectively reason about a problem, identify the cause and come up with a solution. To that end I think interviewing candidates by presenting them with your own mistakes can be incredibly effectively and deeply insightful.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Silly mistakes can make for great interview questions

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮