Adventure with a race-y Set

All credit to: ridiculousfish

What is concurrency, and why should I care? 

Concurrency, isn't that like, in parallel?

Well, no. A concurrent solution may include parallelism, but in itself, modelling a problem this way doesn't necessarily include parallel execution. For example, you can assemble various bits of a car like it's chassis, engine and interior all on their own, you don't have to be doing those things all at the same time. However, in practice, these things are very likely to be done in parallel. 

To say that something can run concurrently means the problem or algorithm can be decomposed into order-independent or partially-ordered components which can be executed out of order or in partial order while the final outcome stays the same (Thanks Wikipedia).

type Test struct {
    val string
    result chan bool
}

type RaceSet struct {
  set map[string] bool
  addChan chan string
  testChan chan Test
}

func (set *RaceSet) run() {
  for {
    select {
      case toadd := <- set.addChan:
        set.set[toadd] = true
      case testreq := <- set.testChan:
        testreq.result <- set.set[testreq.val]
    }
  }
}

func (set *RaceSet) getAny(strings []string) bool {
    result := false
    recvChan := make(chan bool)
    for _, s := range strings {
        request := Test{s, recvChan}
        set.testChan <- request
    }
    for i := 0; i < len(strings); i++ {
        result = result || <- recvChan
    }
    return result
}

func newSet() (result RaceSet) {
  result.set = make(map[string] bool)
  result.addChan = make(chan string, 16)
  result.testChan = make(chan Test, 16)
  go result.run()
  return
}

The Buggy Set

func (set *RaceSet) getAny(strings []string) bool {
    result := false
    recvChan := make(chan bool)
    for _, s := range strings {
        request := Test{s, recvChan}
        set.testChan <- request
    }
    for i := 0; i < len(strings); i++ {
        result = result || <- recvChan
    }
    return result
}

Simple Bug #1

Despite looking ok, the above code will execute successfully for the first call to getAny, but fail for all subsequent calls.

func (set *RaceSet) getAny(strings []string) bool {
    result := false
    recvChan := make(chan bool, len(strings))
    for _, s := range strings {
        request := Test{s, recvChan}
        set.testChan <- request
    }
    for i := 0; i < len(strings); i++ {
        result = result || <- recvChan
    }
    return result
}

Not so simple bug #2 -- Deadlock

We have enough room on the receiving channel for the output from the set's tester channel, but what happens if we don't have enough room on the sending channel? What happens if this line blocks: 

func (set *RaceSet) getAny(strings []string) bool {
    result := false
    recvChan := make(chan bool, len(strings))
    for _, s := range strings {
        request := Test{s, recvChan}
        set.testChan <- request
    }
    for i := 0; i < len(strings); i++ {
        result = result || <- recvChan
    }
    return result
}
set.testChan <- request

Looking at the full code below, it becomes clear that 2 goroutines calling getAny separately might go into deadlock if neither can get all of it's values on the channel first. Both will essentially wait for the buffer to clear before they can proceed, effectively deadlocking. 

But wait, there's more! Call now and get one race condition for free  

There's a race condition in this code as well. What would happen if a routine called add, and the same or another routine called test? Depending on the order in which those 2 are processed, the results might be wrong.

 

Contrary to intuition, the order in which the calls are dequeued in the system is not guaranteed to be the same order in which they were queued. This is particularly noticeable under serious load, however would easily escape detection in testing environments.

It's a bad one. 

Some quick conclusions

1. add followed by get will possible return false on the newly added       value based on execution order.

2. getAny worked the first time, but not on the subsequent calls.  

3. getAny fails for arrays larger than 16 elements.

4. getAny might fail on any size at all when accessing concurrently.         Depends on luck.

Concurrency - Adventure with race-y set

By signupskm

Concurrency - Adventure with race-y set

  • 1,203