英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论