sync.Cond with Wait method in Go

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

sync.Cond with Wait method in Go

问题

我在文档中阅读到了一个不寻常的案例,涉及到了sync.Cond:

因为在等待期间,c.L没有被锁定,所以调用者通常不能假设在Wait返回时条件为真。相反,调用者应该在一个循环中等待:

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

... 利用条件 ...
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] 数据发布者\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()
}
英文:

I read unusual case in documentation sync.Cond:

> Because c.L is not locked while Wait is waiting, the caller typically
> cannot assume that the condition is true when Wait returns. Instead,
> the caller should Wait in a loop:

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

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

I don't understand the idea of it... For example in my code, do I need to have loop or not?

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()
}

答案1

得分: 2

因为在等待期间,Wait 调用时 c.L 没有被锁定,所以调用者通常不能假设在 Wait 返回时条件为真。相反,调用者应该在一个循环中调用 Wait

当你在 sync.Cond 上调用 Wait() 时,它会释放关联的锁,并暂停调用的 goroutine 的执行,直到另一个 goroutine 在相同的 sync.Cond 上调用 Signal 或 Broadcast。

Wait 返回时(表示已经收到信号),它会重新获取锁,然后返回给调用者。然而,由于在 Wait 等待期间锁被释放,调用者等待的条件可能在调用 Wait 和返回之间发生了变化。

例如,假设你有一个共享缓冲区,多个 goroutine 在读取和写入它。你可以使用 sync.Cond 来等待直到缓冲区不为空:

c.L.Lock()
for len(buffer) == 0 {
    c.Wait()
}
// ... 从缓冲区读取 ...
c.L.Unlock()

当缓冲区为空时调用 Wait,期望它在另一个 goroutine 向缓冲区添加一个项后返回。然而,在 Wait 返回之后并且在你的 goroutine 再次开始执行之前,另一个 goroutine 可能已经从缓冲区中消费了该项,使得缓冲区再次为空。

因此,当 Wait 返回时,你不能假设 len(buffer) != 0,即使这是你等待的条件。相反,在继续之前,你应该再次在循环中检查条件,就像示例代码中所示。

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

> 例如,在我的代码中,我需要有循环吗?

是的,你的 subscribe 函数需要有一个循环。

在你的情况下,你正在等待直到 data map 的长度变为零。然而,你的 publisher 函数正在向 map 添加一个元素,所以条件 len(data) != 0 总是为真。这将导致 Wait 函数永远不会被触发。

然而,如果你要检查可能已经更新多次的条件,循环是必要的。当调用 Wait 时,它会释放锁并暂停调用的 goroutine 的执行。稍后,当另一个 goroutine 调用 Broadcast 或 Signal 时,Wait 调用返回并重新获取锁。此时,goroutine 等待的条件可能不再为真,这就是为什么通常应该在循环中调用 Wait 的原因。

简而言之,在你的代码的当前状态下,不需要循环,因为根据代码,你的条件(data map 的长度变为零)永远不会发生。但是,如果你改变条件或添加更多逻辑,可能需要使用循环。

如果你将你的 publisher 函数更改为清空 map,并且你希望确保 subscriber 仅在 map 为空时处理数据,你可以这样使用循环:

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

这个循环将确保只有在 map 为空时才开始处理数据。如果 map 不为空,它将在 Wait 返回时继续等待。

英文:

> Because c.L is not locked while Wait is waiting, the caller typically
> cannot assume that the condition is true when Wait returns. Instead,
> the caller should Wait in a loop:

When you call Wait() on a sync.Cond, it releases the associated lock and suspends execution of the calling goroutine until another goroutine calls Signal or Broadcast on the same sync.Cond.

When Wait returns (meaning it has been signalled), it re-acquires the lock before returning to the caller. However, since the lock was released while Wait was waiting, it's possible that the condition the caller was waiting for has changed between when Wait was called and when it returned.

For example, let's say you have a shared buffer and multiple goroutines are reading from and writing to it. You might use a sync.Cond to wait until the buffer is not empty:

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

You call Wait when the buffer is empty, expecting that it will return once another goroutine has added an item to the buffer. However, it's possible that after Wait returns and before your goroutine starts executing again, another goroutine could have already consumed the item from the buffer, leaving it empty again.

Because of this, when Wait returns, you can't assume that len(buffer) != 0, even though that's the condition you were waiting for. Instead, you should check the condition again in a loop, as shown in the example code, to make sure it's still true before you proceed.

The loop (for len(buffer) == 0) ensures that if the condition is not met when Wait returns, it will simply wait again until the condition is met.

> For example in my code, do I need to have loop or not?

Yes, your subscribe function would need to have a loop.

In your case, you are waiting until the length of data map becomes zero. However, your publisher function is adding an element to the map, so the condition len(data) != 0 will always be true. This would make the Wait function to be never triggered.

However, if you were checking for a condition that might have been updated multiple times, a loop would be necessary. When Wait is called, it releases the lock and suspends the execution of the calling goroutine. Later, when another goroutine calls Broadcast or Signal, the Wait call returns and re-acquires the lock. At this point, the condition that the goroutine was waiting for might no longer be true, which is why you should typically call Wait in a loop.

In a nutshell, in the current state of your code, loop is not needed because your condition (length of data map becomes zero) will never happen based on the code. But if you change the condition or add more logic, the usage of a loop can be required.

If you change your publisher function to empty the map and you want to ensure that subscriber processes the data only when the map is empty, you would use a loop in this way:

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()
}

This loop will ensure that the data processing doesn't start until the map is empty. It will keep waiting if the map is not empty whenever Wait returns.

答案2

得分: 0

这段代码的作用是在等待条件时解锁锁(c.L),然后等待通知,最后再次锁定锁。在c.L.Unlock()c.L.Lock()之间,其他goroutine可能会更改数据(条件)。因此,文档建议检查条件(数据)是否发生了变化。

你可以查看Cond.Wait源代码来了解更多信息。

英文:

It is not obvious, but if you check Cond.Wait source code you can see this:

c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()

As you can see it unlocks locker (c.L, in your case it is sync.WaitGroup{}) and then waits for notification (runtime_notifyListWait(&c.notify, t)) and then locks it again.
So your data (condition) may be changed in some other goroutine between c.L.Unlock() and c.L.Lock. This is why documentation proposes to check whether condition (data) was changed or not.

huangapple
  • 本文由 发表于 2023年7月22日 15:59:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/76742838.html
匿名

发表评论

匿名网友

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

确定