【问题标题】:Should we synchronize variable assignment in goroutine?我们应该在 goroutine 中同步变量赋值吗?
【发布时间】:2019-08-09 18:35:24
【问题描述】:

假设我声明了两个映射并希望将其分配到错误组中的两个不同的 goroutine 中。我不执行任何读/写。 我应该用lock 保护分配操作还是可以省略它?

UPD3:在Java Concurrency In PracticeBrian Goetz's Part I Chapter 3 Shared Objects,提到:

锁定不仅仅是互斥;也是记忆 能见度。确保所有线程都看到最新的值 共享可变变量,读写线程必须 在公共锁上同步。

var (
    mu     sync.Mutex
    one    map[string]struct{}
    two    map[string]struct{}
)
g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
    resp, err := invokeFirstService(gctx, request)
    if err != nil {
        return err
    }
    mu.Lock()
    one = resp.One
    mu.Unlock()
    return nil
})

g.Go(func() error {
    resp, err := invokeSecondService(gctx, request)
    if err != nil {
        return err
    }
    mu.Lock()
    two = resp.Two
    mu.Unlock()
    return nil
})
if err := g.Wait(); err != nil {
    return err
}
// UPD3: added lock and unlock section
m.Lock()
defer m.Unlock()
performAction(one, two)

UPD:添加了更多关于变量的上下文

UPD2:我的疑虑是什么:我们有 3 个 goroutine - 父级和错误组中的两个。不能保证我们的父 goroutine 共享内存在 errgroup goroutine 完成后获得最后一次更新,直到我们用内存屏障包装对共享内存的访问

【问题讨论】:

    标签: go concurrency synchronization locking


    【解决方案1】:

    Group.Wait() 阻塞,直到来自Group.Go() 方法的所有函数调用都返回,所以这是一个同步点。这可确保在完成对 onetwo 的任何写入之前,performAction(one, two) 不会启动,因此在您的示例中,互斥锁是不必要的。

    g, gctx := errgroup.WithContext(ctx)
    g.Go(func() error {
        // ...
        one = resp.One
        return nil
    })
    
    g.Go(func() error {
        // ...
        two = resp.Two
        return nil
    })
    
    if err := g.Wait(); err != nil {
        return err
    }
    // Here you can access one and two safely:
    performAction(one, two)
    

    如果您要从其他 goroutine 访问 onetwo 而编写它们的 goroutine 同时运行,那么是的,您需要锁定它们,例如:

    // This goroutine runs concurrently, so all concurrent access must be synchronized:
    go func() {
        mu.Lock()
        fmt.Println(one, two)
        mu.Unlock()
    }()
    
    g, gctx := errgroup.WithContext(ctx)
    g.Go(func() error {
        // ...
        mu.Lock()
        one = resp.One
        mu.Unlock()
        return nil
    })
    
    g.Go(func() error {
        // ...
        mu.Lock()
        two = resp.Two
        mu.Unlock()
        return nil
    })
    
    if err := g.Wait(); err != nil {
        return err
    }
    // Note that you don't need to lock here
    // if the first concurrent goroutine only reads one and two.
    performAction(one, two)
    

    还要注意,在上面的例子中你可以使用sync.RWMutex,在读取它们的goroutine中,RWMutex.RLock()RWMutex.RUnlock()也足够了。

    【讨论】:

    • 我的疑虑是什么:我们有 3 个 goroutine:父级和错误组中的两个。不能保证我们的父 goroutine 共享内存在 errgroup goroutine 完成后得到最后一次更新,直到我们用内存屏障包装对共享内存的访问
    • @SergiiGetman 如果你使用同步,你有你的保证。 sync.Mutex 是一种工具,GroupWaitGroup 也是。显然 1 个工具就足够了,您不必使用多个工具。阅读The Go Memory Model了解详情。
    • 我已经更新了在代码示例中添加锁定/解锁阅读的问题并备注:reading and writing threads must synchronize on a common lock。我知道这是与 java 相关的评论,但我建议它也适用于 golang。如果不相关,请纠正我
    • @SergiiGetman 这不适用于 Go。在 Go 中,您需要建立 happens-before 关系。在问题的代码中存在这样的关系:g.Wait() 调用将在 performAction() 调用之前发生(g.Wait() 在其 goroutine 运行之前不会返回),因此 goroutine 中的写入保证会发生并进行在调用 performAction() 之前可见,或者更准确地说,它的参数被评估。这种发生前的关系为您提供了保证,如 Go 内存模型(我之前建议阅读)中所述。
    • 非常感谢您的解释!不幸的是,它在我看来仍然很复杂。在您提供的链接中是 Goroutine 销毁一章:如果一个 goroutine 的效果必须由另一个 goroutine 观察,请使用锁或通道通信等同步机制来建立相对顺序。这就是为什么我仍然感到困惑。
    【解决方案2】:

    在这种情况下,只有一个 goroutine 可以访问地图。我认为你不需要锁。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-06-25
      • 1970-01-01
      • 2021-10-03
      • 1970-01-01
      • 2020-03-17
      • 1970-01-01
      • 2021-12-29
      相关资源
      最近更新 更多