我在文档同步中读到了异常 case .结论:

由于等待时C.L未锁定,因此调用方通常 当WAIT返回时,不能假定条件为真.相反, 调用方应该循环等待:

c.L.Lock()
for !condition() {
    c.Wait()
}

... make use of condition ...
c.L.Unlock()

我不明白这是怎么回事.例如,在我的代码中,我是否需要有循环?

func subscribe(name string, data map[string]string, c *sync.Cond) {
    c.L.Lock()

    for len(data) != 0 {
        c.Wait()
    }

    fmt.Printf("[%s] %s\n", name, data["key"])

    c.L.Unlock()
}

func publish(name string, data map[string]string, c *sync.Cond) {
    time.Sleep(time.Second)

    c.L.Lock()
    data["key"] = "value"
    c.L.Unlock()

    fmt.Printf("[%s] data publisher\n", name)
    c.Broadcast()
}

func main() {
    data := map[string]string{}
    cond := sync.NewCond(&sync.Mutex{})

    wg := sync.WaitGroup{}
    wg.Add(3)

    go func() {
        defer wg.Done()
        subscribe("subscriber_1", data, cond)
    }()

    go func() {
        defer wg.Done()
        subscribe("subscriber_2", data, cond)
    }()

    go func() {
        defer wg.Done()
        publish("publisher", data, cond)
    }()

    wg.Wait()
}

推荐答案

由于等待时C.L未锁定,因此调用方通常 当WAIT返回时,不能假定条件为真.相反, 调用方应该循环等待:

当您在sync.Cond上调用Wait()时,它会释放关联的锁并暂停调用Goroutine的执行,直到另一个Goroutine在同一sync.Cond上调用信号或广播.

当Wait返回时(意味着它已收到信号),它会在返回到调用方之前重新获取锁.但是,由于锁是在等待时释放的,因此调用方等待的条件可能在调用Wait和返回之间发生了变化.

例如,假设您有一个共享缓冲区,并且多个Goroutine正在对其进行读写.您可以使用sync.Cond来等待,直到缓冲区不为空:

c.L.Lock()
for len(buffer) == 0 {
    c.Wait()
}
// ... read from buffer ...
c.L.Unlock()

当缓冲区为空时调用Wait,期望一旦另一个Goroutine向缓冲区添加了项,它就会返回.然而,有可能在返回Wait之后,在您的Goroutine再次开始执行之前,另一个Goroutine可能已经从缓冲区中消费了该项,并将其再次留空.

正因为如此,当Wait返回时,你不能假设len(buffer) != 0,即使这是你等待的条件.相反,您应该在循环中再次判断条件,如示例代码所示,以确保在继续之前它仍然为真.

循环(for len(buffer) == 0)确保如果在等待返回时不满足条件,则它将简单地再次等待,直到满足条件.

例如,在我的代码中,我是否需要有循环?

是的,您的订阅函数需要有一个循环.

在您的例子中,您是在等待数据映射的长度变为零.但是,您的publisher函数将向映射中添加一个元素,因此条件len(data) != 0将始终为真.这将使Wait功能永远不会被触发.

但是,如果要判断可能已多次更新的条件,则需要进行循环.当调用Wait时,它释放锁并挂起调用Goroutine的执行.稍后,当另一个Goroutine调用Broadcast或Signal时,Wait调用返回并重新获取锁.此时,Goroutine等待的条件可能不再为真,这就是为什么您通常应该在循环中调用Wait.

简而言之,在代码的当前状态下,不需要循环,因为您的条件(数据映射的长度变为零)永远不会基于代码发生.但如果更改条件或添加更多逻辑,则可能需要使用循环.

如果您将发布者功能更改为清空 map ,并且希望确保订阅者仅在 map 为空时处理数据,则可以按以下方式使用循环:

func subscribe(name string, data map[string]string, c *sync.Cond) {
    c.L.Lock()
    for len(data) != 0 {
        c.Wait()
    }
    // ... process data ...
    c.L.Unlock()
}

该循环将确保数据处理在映射为空之前不会开始.每当等待返回时,如果 map 不为空,它将继续等待.

Go相关问答推荐

使用Gorm创建自定义连接表

Zitadel示例Go Webapp加密密钥

一种基于时间的Golang函数节制器

从 wincrypt API 到 Go 的 RC2 解密

使用 httptest 对 http 请求进行单元测试重试

有没有办法在 Golang 中使用带有 go-simple-mail lib 的代理?

使用自定义处理程序 nats golang 保留订阅方法

如何仅提取时间作为持续时间

GRPC 元数据未在 Go 中更新

使用innerxml在 Go 中编码 XML 是否仅适用于某些类型?

如何将一片十六进制字节转换为整数

GqlGen - 在字段解析器中访问查询输入参数

使用 package`regexp` 查找 Golang 中的所有 mactch 子字符串,但得到意外结果

函数超时和 goroutine 泄漏

Gorm 在保存/创建时序列化 struct

如何在gin中获取参数值数组

递归数据 struct 解组在 Go Lang Protobuf 中给出错误无法解析无效的线格式数据

Golang LinkedList 删除第一个元素

为什么 Go 不允许将一个泛型分配给另一个泛型?

有没有办法停止long blocking 函数?