英文:
Go using timeouts with channels
问题
我正在使用goroutines/channels来检查一组URL是否可达。以下是我的代码。这段代码似乎总是返回true。为什么超时的情况没有被执行?目标是即使其中一个URL不可达也返回false。
import "fmt"
import "time"
func check(u string) bool {
time.Sleep(4 * time.Second)
return true
}
func IsReachable(urls []string) bool {
ch := make(chan bool, 1)
for _, url := range urls {
go func(u string) {
select {
case ch <- check(u):
case <-time.After(time.Second):
ch <- false
}
}(url)
}
return <-ch
}
func main() {
fmt.Println(IsReachable([]string{"url1"}))
}
请注意,这段代码中的check
函数总是返回true
,因此无论如何都会返回true
。你需要在check
函数中实现检查URL是否可达的逻辑,并根据实际情况返回true
或false
。另外,你可以根据需要调整超时时间。
英文:
I am using goroutines/channels to check if list of urls are reachable. Here is my code. This seems to always return true. Why is the timeout case not getting executed? The goal is to return false even if one of the urls is not reachable
import "fmt"
import "time"
func check(u string) bool {
time.Sleep(4 * time.Second)
return true
}
func IsReachable(urls []string) bool {
ch := make(chan bool, 1)
for _, url := range urls {
go func(u string) {
select {
case ch <- check(u):
case <-time.After(time.Second):
ch<-false
}
}(url)
}
return <-ch
}
func main() {
fmt.Println(IsReachable([]string{"url1"}))
}
答案1
得分: 28
check(u)
将在当前的goroutine中休眠,也就是正在运行func
的那个goroutine。select
语句只有在返回后才会正确运行,而在那时,两个分支都是可运行的,运行时可以选择任何一个。
你可以通过在另一个goroutine中运行check
来解决这个问题:
package main
import "fmt"
import "time"
func check(u string, checked chan<- bool) {
time.Sleep(4 * time.Second)
checked <- true
}
func IsReachable(urls []string) bool {
ch := make(chan bool, 1)
for _, url := range urls {
go func(u string) {
checked := make(chan bool)
go check(u, checked)
select {
case ret := <-checked:
ch <- ret
case <-time.After(1 * time.Second):
ch <- false
}
}(url)
}
return <-ch
}
func main() {
fmt.Println(IsReachable([]string{"url1"}))
}
看起来你想要检查一组URL的可达性,并且如果其中一个可用,则返回true。如果超时时间与启动goroutine所需的时间相比较长,你可以通过只有一个超时来简化这个问题,用于所有URL的超时。但是我们需要确保通道足够大,可以容纳所有检查的答案,否则那些没有“获胜”的答案将永远阻塞:
package main
import "fmt"
import "time"
func check(u string, ch chan<- bool) {
time.Sleep(4 * time.Second)
ch <- true
}
func IsReachable(urls []string) bool {
ch := make(chan bool, len(urls))
for _, url := range urls {
go check(url, ch)
}
time.AfterFunc(time.Second, func() { ch <- false })
return <-ch
}
func main() {
fmt.Println(IsReachable([]string{"url1", "url2"}))
}
英文:
check(u)
will sleep in the current goroutine, i.e. the one that's running func
. The select
statement is only run properly once it returns, and by that time, both branches are runnable and the runtime can pick whichever one it pleases.
You can solve it by running check
inside yet another goroutine:
package main
import "fmt"
import "time"
func check(u string, checked chan<- bool) {
time.Sleep(4 * time.Second)
checked <- true
}
func IsReachable(urls []string) bool {
ch := make(chan bool, 1)
for _, url := range urls {
go func(u string) {
checked := make(chan bool)
go check(u, checked)
select {
case ret := <-checked:
ch <- ret
case <-time.After(1 * time.Second):
ch <- false
}
}(url)
}
return <-ch
}
func main() {
fmt.Println(IsReachable([]string{"url1"}))
}
It seems you want to check reachability of a set of URLs, and return true if one of them is available. If the timeout is long compared to the time it takes to spin up a goroutine, you could simplify this by having just one timeout for all URLs together. But we need to make sure that the channel is large enough to hold the answers from all checks, or the ones that don't "win" will block forever:
package main
import "fmt"
import "time"
func check(u string, ch chan<- bool) {
time.Sleep(4 * time.Second)
ch <- true
}
func IsReachable(urls []string) bool {
ch := make(chan bool, len(urls))
for _, url := range urls {
go check(url, ch)
}
time.AfterFunc(time.Second, func() { ch <- false })
return <-ch
}
func main() {
fmt.Println(IsReachable([]string{"url1", "url2"}))
}
答案2
得分: 8
这个函数总是返回true的原因是你在select
语句中调用了check(u)
。你需要在一个go协程中调用它,然后使用select来等待结果或超时。
如果你想并行检查多个URL的可达性,你需要重构你的代码。
首先创建一个函数来检查一个URL的可达性:
func IsReachable(url string) bool {
ch := make(chan bool, 1)
go func() { ch <- check(url) }()
select {
case reachable := <-ch:
return reachable
case <-time.After(time.Second):
// 调用超时
return false
}
}
然后从一个循环中调用这个函数:
urls := []string{"url1", "url2", "url3"}
for _, url := range urls {
go func() { fmt.Println(IsReachable(url)) }()
}
英文:
The reason this always returns true is you are calling check(u)
within your select
statement. You need to call it within a go routine and then use a select to either wait for the result or timeout.
In case you want to check the reachability of multiple URLs in parallel you need to restructure your code.
First create a function which checks the reachability of one URL:
func IsReachable(url string) bool {
ch := make(chan bool, 1)
go func() { ch <- check(url) }()
select {
case reachable := <-ch:
return reachable
case <-time.After(time.Second):
// call timed out
return false
}
}
Then call this function from a loop:
urls := []string{"url1", "url2", "url3"}
for _, url := range urls {
go func() { fmt.Println(IsReachable(url)) }()
}
答案3
得分: 1
将以下代码行进行更改:
ch := make(chan bool, 1)
更改为:
ch := make(chan bool)
你之前创建了一个异步(非阻塞)通道,但是你需要一个阻塞通道才能使其正常工作。
英文:
change the line
ch := make(chan bool, 1)
to
ch := make(chan bool)
You did open a asynchronous (= non blocking) channel, but you need a blocking channel to get it work.
答案4
得分: 0
在这种情况下,返回true的结果是确定性的,而不是运行时随机选择的,因为通道中只有true值可用(无论需要多长时间才能可用!)。false的结果永远不会在通道中可用,因为time.After()调用语句根本没有机会被执行!
在这个select语句中,它看到的第一行可执行代码是check(u)的调用,而不是第一个case分支中的通道发送调用,或者其他任何调用!只有在第一个check(u)执行返回后,才会检查并调用select分支,此时true的值已经被推送到第一个分支的通道中,因此在select语句中没有通道阻塞,它可以迅速地完成其目的,而无需检查其余的分支!
所以在这种情况下,使用select似乎不太正确。
select分支应该直接监听通道的发送和接收值,或者在必要时使用默认值来避免阻塞。
所以修复方法就是,将长时间运行的任务或进程放入一个单独的goroutine中,并让它将结果发送到通道中,然后在主goroutine(或任何其他需要该值的例程)中,使用select分支来监听特定通道上的值,或者使用time.After(time.Second)调用提供的通道。
基本上,这行代码:case ch <- check(u)在发送值到通道方面是正确的,但它并不是用于其预期的用途(即阻塞该分支),因为该case ch <-并没有被阻塞(check(u)花费的时间都发生在通道涉及之前),因为在一个单独的goroutine中,也就是主goroutine:return <-ch,它已经准备好在值被推送时读取该值。这就是为什么第二个case分支中的time.After()调用语句甚至没有机会被评估的原因,在第一次执行中!
可以参考这个示例来了解一个简单的解决方案,即正确使用select和单独的goroutine的方法:https://gobyexample.com/timeouts
英文:
The result of true being returned here is deterministic in this scenario, it's not a random one picked up by the runtime, because there's only true value available (however long it may take for it to become available!) being sent into the channel, the false result would never be available for the channel since the time.After() call statement would never get the chance to be executed in the first place!
In this select, the first executable line it sees is check(u) call, not the channel sending call in the first case branch, or any other call at all! And it's only after this first check(u) execution has returned here, would select branch cases get checked and called upon, by which point, the value of true is already to be pushed into the first branch case channel, so no channel blocking here to the select statement, the select can fulfil its purpose promptly here without needing to check its remaining branch cases!
so looks like it's the use of select here that wouldn't seem quite correct in this scenario.
the select branch cases are supposed to listen to channel sending and receiving values directly, or optionally with a default to escape the blocking when necessary.
so the fix is as some people pointed out here already, putting the long running task or process into a separate goroutine, and have it send result into channel,
and then in the main goroutine (or whichever other routine that needs that value off the channel), use the select branch cases to either listen on that specific channel for a value, or on the channel provided by the time.After(time.Second) call.
Basically, this line: case ch <- check(u) is correct in the sense of sending a value into a channel, but it's just not for its intended use (i.e. blocking this branch case), because the case channel<- is not being blocked there at all (the time check(u) spends on is all happening before the channel gets involved), since in a separate goroutine, aka, the main one: return <-ch, it's already ready to read that value whenever it gets pushed through. That is why time.After() call statement in the second case branch would never even get a chance to be evaluated, in the first instance!
see this example for a simple solution, ie. the correct use of a select in conjunction of separate goroutines: https://gobyexample.com/timeouts
答案5
得分: 0
如果有用的话,这是@Thomas的答案的一个泛化版本,由@mh-cbon简化:
func WithTimeout(delegate func() interface{}, timeout time.Duration) (ret interface{}, ok bool) {
ch := make(chan interface{}, 1) // buffered
go func() { ch <- delegate() }()
select {
case ret = <-ch:
return ret, true
case <-time.After(timeout):
}
return nil, false
}
然后你可以调用WithTimeout
来超时任何函数:
if value, ok := WithTimeout(myFunc, time.Second); ok {
// 返回了
} else {
// 没有返回
}
像这样等待一个通道:
if value, ok := WithTimeout(func() interface{} { return <-inbox }, time.Second); ok {
// 返回了
} else {
// 没有返回
}
像这样尝试发送:
_, ok = WithTimeout(func() interface{} { outbox <- myValue; return nil }, time.Second)
if !ok {
// ...
}
英文:
In case it's useful, here's a generalised version of @Thomas 's answer, much simplified by @mh-cbon
func WithTimeout(delegate func() interface{}, timeout time.Duration) (ret interface{}, ok bool) {
ch := make(chan interface{}, 1) // buffered
go func() { ch <- delegate() }()
select {
case ret = <-ch:
return ret, true
case <-time.After(timeout):
}
return nil, false
}
Then you can call to 'timeout' any function
if value,ok := WithTimeout(myFunc, time.Second); ok {
// returned
} else {
// didn't return
}
Call like this to wait for a channel
if value,ok := WithTimeout(func()interface{}{return <- inbox}, time.Second); ok {
// returned
} else {
// didn't return
}
Like this to try sending
_,ok = WithTimeout(func()interface{}{outbox <- myValue; return nil}, time.Second)
if !ok{...
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论