Here is one case of Data Race Patterns in Go
Mixed use of message passing (channels) and shared memory makes code complex and susceptible to data races
func (f *Future) Start() {
go func() {
resp, err := f.f()
f.resp = resp
f.err = err // data race
f.ch <- struct{}{} // may block forever
}()
}
func (f *Future) Wait(ctx context.Context) error {
select {
case <-f.ch:
return nil
case <-ctx.Done():
f.err = ctx.Err() // data race
return ctx.Err()
}
}
Here is one data race for f.err
, which could be accessed both in Start
and Wait
.
Another issue is that f.ch <- struct{}{}
may block forever when ctx.Done()
happened before f.ch <- struct{}{}
.
What is the elegant way to handle it in Golang?
The idea comes to my mind first as below, add context to Start
function
func (f *Future) Wait(ctx context.Context) error {
select {
case <-f.ch:
return nil
case <-ctx.Done():
f.err = ctx.Err()
return ctx.Err()
}
}
func (f *Future) StartWithContext(ctx context.Context) {
go func() {
resp, err := f.f()
select {
case <-ctx.Done():
return
default:
f.resp = resp
f.err = err
f.ch <- struct{}{}
}
}()
}
We want to know if something missing in my solution? or is there a better solution to this issue?
CodePudding user response:
There is still data race in the second code snippet. It is possible for StartWithContext
to fall to default
case first, and then Wait
falling into the ctx.Done()
case.
You have a shared memory problem, and the most obvious way to deal with it is with a mutex. Also note that you are using f.ch
as a done-channel, so:
type Future struct {
sync.Mutex
...
}
func (f *Future) Start() {
go func() {
resp, err := f.f()
f.Lock()
f.resp = resp
f.err = err
f.Unlock()
close(f.ch)
}()
}
func (f *Future) Wait(ctx context.Context) error {
select {
case <-f.ch:
return nil
case <-ctx.Done():
f.Lock()
f.err = ctx.Err()
f.Unlock()
return ctx.Err()
}
}
CodePudding user response:
Use the channel to send the error to the waiter.
Change Future to:
type Future struct {
ch chan error
// other fields as before
}
Send the error in Start:
func (f *Future) Start() {
go func() {
resp, err := f.f()
f.resp = resp
f.ch <- err
}()
}
Receive the error in Wait:
func (f *Future) Wait(ctx context.Context) error {
select {
case err := <-f.ch:
return err
case <-ctx.Done():
f.err = ctx.Err()
return ctx.Err()
}
}
Create a buffered channel to prevent the goroutine in Start from blocking forever when the context is canceled.
f.ch = make(chan error, 1)