Last active
July 14, 2017 18:30
-
-
Save Groxx/473fd6771013557c07d6b67aefff9cdb to your computer and use it in GitHub Desktop.
Golang channel closes are threadsafe?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
The race detector complains about code like this: | |
c := make(chan struct{}, 1) | |
go func() { c <- struct{}{} }() | |
go func() { close(c) }() | |
go func() { _ = <- c }() | |
But I have yet to see this test suite fail (which would imply undetectably losing data). | |
If it doesn't ever lose data, is it actually worthy of a race detector failure? | |
Obviously it's risky for most code, as it can panic and cannot be synchronized e.g. while | |
blocked on a send. | |
But it also means it's possible to give strong cross-thread guarantees like "must stop | |
enqueueing things" + "must process all enqueued things" with a simple: | |
close(queue) | |
for item := range queue { ... } | |
which seems very useful. Enqueue-ers just have to guard against panics to know if it was | |
sent or not. | |
--------- | |
I have nothing currently to check for *corruption* of data, but after reading through the | |
go 1.8 chan source: https://github.com/golang/go/blob/release-branch.go1.8/src/runtime/chan.go | |
it seems pretty straightforwardly safe, unless there are exceptions to this rule outside the | |
file(s) I read. or subtleties that are not obvious. | |
*/ | |
package main | |
import ( | |
"math/rand" | |
"testing" | |
"github.com/stretchr/testify/assert" | |
) | |
// This test intentionally violates the race detector. | |
// | |
// (Attempt to) prove that closing a channel is threadsafe, and does not result in invisibly lost messages. | |
// I.e. sends either succeed (and are retrievable) or panic, no gaps. | |
// | |
// Though it is not proof, I have yet to see this fail. | |
func TestCloseIsThreadsafe(t *testing.T) { | |
t.Parallel() | |
a := assert.New(t) | |
tries := 1000000 | |
sent := 0 | |
panicked := 0 | |
received := 0 | |
for i := 0; i < tries; i++ { | |
c := make(chan bool, 1) | |
result := make(chan bool) | |
sender := func() { | |
defer func() { | |
if err := recover(); err != nil { | |
result <- false | |
} else { | |
result <- true | |
} | |
}() | |
c <- true | |
} | |
closer := func() { | |
close(c) | |
} | |
// demonstrating that order does not matter | |
if rand.Int()%2 == 0 { | |
go sender() | |
go closer() | |
} else { | |
go closer() | |
go sender() | |
} | |
if <-result { | |
sent += 1 | |
} else { | |
panicked += 1 | |
} | |
select { | |
case _, ok := <-c: | |
if ok { | |
received += 1 | |
} | |
default: | |
} | |
} | |
// assert not flaky | |
a.NotEqual(0, sent, "Should have sent at least once, out of %v tries", tries) | |
a.NotEqual(0, panicked, "Should have panicked at least once, out of %v tries", tries) | |
// actual assertion | |
a.Equal(sent, received, "Sent and received should be equal despite panics") | |
// Uncomment to show results. Typically sends dominate panics | |
//a.Fail( | |
// "Fake failure to show results", | |
// "Sent %v times, received %v times, panicked %v times, out of %v attempts", | |
// sent, received, panicked, attempts, | |
//) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment