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.
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
}