Home > Software engineering >  Elegant way to handle data race caused by passing channel and shared memory
Elegant way to handle data race caused by passing channel and shared memory

Time:07-21

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)
  • Related