Go内存模型:如何使用适当的同步方式给字段赋值

huangapple go评论81阅读模式
英文:

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

问题

以下是我在Go中实现Promise的核心部分。

// Promise表示对函数调用的未来结果。
type promise struct {
	// state是此Promise的当前状态。
	state int32
	// done在执行完成时关闭,以解除并发等待者的阻塞。
	done chan struct{}
	// 用于填充结果的函数。
	function Function
	// 执行完成时设置结果。
	outcome Outcome
}

// get返回与Promise关联的值。
//
// 对给定Promise的所有promise.get调用都返回相同的结果,
// 但是该函数最多只会被调用(完成)一次。
//
// - 如果底层函数尚未调用,则会调用它。
// - 如果ctx被取消,则get返回(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启动p.function并返回结果。
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 // 帮助GC
		close(p.done)
	}()

	return p.wait(ctx)
}

// wait等待计算出的值,或ctx被取消。
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))
}

今天我的同事给我发了一篇关于Go内存模型的文章。在文章中,作者包含了以下示例,并提到g可能会先打印2然后打印0

var a, b int

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

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

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

> 存在竞争的程序是不正确的,并且可能会展示非顺序一致的执行。特别要注意的是,读取r可能会观察到与r并发执行的任何写入w写入的值。即使发生这种情况,也不能保证在r之后发生的读取将观察到在w之前发生的写入。

直到现在,我一直认为在关闭done通道之前设置变量可以确保其他例程能够看到该变量的最新值。

然而,上面的示例让我对Go的工作原理产生了疑问,以及使用done通道是否有任何区别。其他例程是否可能检测到done通道已关闭并继续读取尚未更新的字段?

如果您能解释一下我的观点是否仍然正确,我将非常感激。如果我错了,请告诉我正确同步读取和写入字段的方法。

英文:

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 &lt;-p.done:
return p.outcome
case &lt;-ctx.Done():
return Outcome{
Value: nil,
Err:   ctx.Err(),
}
}
}
func (p *promise) changeState(from, to State) bool {
return atomic.CompareAndSwapInt32(&amp;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.

答案1

得分: 0

p.outcome的赋值在close(done)之前进行,因此任何检测到done关闭的goroutine都将看到p.outcome的最新值,因为如果done关闭,那么p.outcome就会在它之前发生。

p.changeState可能存在竞争条件,但你的帖子中没有包含它。

话虽如此,使用通道和goroutine可以以更清晰的方式提供与promise相同的功能:

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

select {
   case <-ctx.Done():
     // 已取消
   case result := <-resultCh:
     // 结果已准备好
}
英文:

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&lt;-someFunc(ctx)
}()
select {
case &lt;-ctx.Done():
// Canceled
case result:=&lt;-resultCh:
// result is ready
}
</details>

huangapple
  • 本文由 发表于 2023年8月8日 23:26:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/76861016.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定