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