Skip to content

Instantly share code, notes, and snippets.

@Groxx
Last active July 14, 2017 18:30
Show Gist options
  • Save Groxx/473fd6771013557c07d6b67aefff9cdb to your computer and use it in GitHub Desktop.
Save Groxx/473fd6771013557c07d6b67aefff9cdb to your computer and use it in GitHub Desktop.
Golang channel closes are threadsafe?
/*
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