Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Go Memory Model: How to assign values to a field with proper synchronization

Below is the core portion of my implementation for Promise in Go.

// A promise represents the future result of a call to a function.
type promise struct {
    // state is the current state of this promise.
    state int32
    // done is closed when execution completes to unblock concurrent waiters.
    done chan struct{}
    // the function that will be used to populate the outcome.
    function Function
    // outcome is set when execution completes.
    outcome Outcome
}

// get returns the value associated with a promise.
//
// All calls to promise.get on a given promise return the same result
// but the function is called (to completion) at most once.
//
// - If the underlying function has not been invoked, it will be.
// - If ctx is cancelled, get returns (nil, context.Canceled).
func (p *promise) get(ctx context.Context) Outcome {
    if ctx.Err() != nil {
        return Outcome{
            Value: nil,
            Err:   ctx.Err(),
        }
    }

    if p.changeState(IsCreated, IsExecuted) {
        return p.run(ctx)
    }

    return p.wait(ctx)
}

// run starts p.function and returns the result.
func (p *promise) run(ctx context.Context) Outcome {
    go func() {
        v, err := doExecute(ctx, p.function)

        p.outcome = Outcome{
            Value: v,
            Err:   err,
        }
        p.function = nil // aid GC
        close(p.done)
    }()

    return p.wait(ctx)
}

// wait waits for the value to be computed, or ctx to be cancelled.
func (p *promise) wait(ctx context.Context) Outcome {
    select {
    case <-p.done:
        return p.outcome

    case <-ctx.Done():
        return Outcome{
            Value: nil,
            Err:   ctx.Err(),
        }
    }
}

func (p *promise) changeState(from, to State) bool {
    return atomic.CompareAndSwapInt32(&p.state, int32(from), int32(to))
}

A colleague gave me a link to the Go Memory Model article today. In the article, the author includes the following example and mentions that it’s possible for g to print 2 and then 0.

var a, b int

func f() {
    a = 1
    b = 2
}

func g() {
    print(b)
    print(a)
}

func main() {
    go f()
    g()
}

Programs with races are incorrect and can exhibit non-sequentially consistent executions. In particular, note that a read r may observe the value written by any write w that executes concurrently with r. Even if this occurs, it does not imply that reads happening after r will observe writes that happened before w.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Up until now, I’ve always thought that setting a variable BEFORE closing the done channel can guarantee that other routines would be able to see the latest value of this variable.

However, the example above made me question my understanding of how Go works and whether using a done channel makes any differences at all. Would it be possible for other routines to detect that the done channel is closed and move on to read the field that has not yet been updated?

I’d be very grateful if you could explain to me if my belief is still correct. If it’s wrong, please show me the right way to synchronize read & write to a field.

>Solution :

The assignment to p.outcome is "sequenced before" close(done), thus any goroutine detecting that done is closed will see the most recent value of p.outcome, because if done is closed, p.outcome happened before it.

p.changeState might have a race in it, but you didn’t include that in your post.

That said, a channel and a goroutine provides the same functionality as a promise, and does it in a cleaner way:

resultCh:=make(chan resultType)
go func() {
   resultCh<-someFunc(ctx)
}()

select {
   case <-ctx.Done():
     // Canceled
   case result:=<-resultCh:
     // result is ready
}
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading