Skip to content

Latest commit

 

History

History
36 lines (26 loc) · 1.74 KB

File metadata and controls

36 lines (26 loc) · 1.74 KB

05. Signal vs payload

Bug

A channel used as a signal (chan struct{}) synchronizes the signal itself, not a payload written separately in a shared field. In go/unsafe/main.go the worker writes Future.err then closes ready, while the consumer's timeout path also writes Future.err. The timeout (5 ms) is shorter than the worker delay (30 ms), so the timeout path writes err = timeout first, then the worker writes again:

select {
case <-f.ready:
    return f.err
case <-time.After(timeout):
    f.err = errors.New("timeout") // second writer, unsynchronized
    return f.err
}

Two unsynchronized writes to the same field: WARNING: DATA RACE, and the printed result is "timeout" even though the worker produced a real error.

Fix

Make the payload the channel message (chan error): signal and value travel together, synchronized by the send/receive. The timeout path returns a local error and never touches shared state, so there is exactly one writer per result. Guarding signal and payload with one mutex is an equivalent alternative.

Language comparison

  • Java: analogue: java/Unsafe.java uses a CountDownLatch as signal and an unsynchronized err field; java/Fixed.java sends the payload as the CompletableFuture result.
  • C++: analogue: cpp/unsafe.cpp uses a std::condition_variable plus a separate err; cpp/fixed.cpp protects err and a done flag with the same mutex and notifies under the lock.

Run

go run -race ./scenarios/05-signal-vs-payload/go/unsafe
go test -race ./scenarios/05-signal-vs-payload/go/fixed
SCENARIO_LANG=java make demo PATTERN=05-signal-vs-payload
cmake -B build -DRACE_SANITIZER=ON && cmake --build build --parallel
SCENARIO_LANG=cpp make demo PATTERN=05-signal-vs-payload