使用通道超时

  • 本文关键字:超时 通道 go channel
  • 更新时间 :
  • 英文 :


我正在使用gooutines/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(u)将在当前例程中休眠,即运行func的例程。select语句只有在它返回时才正常运行,到那时,两个分支都是可运行的,运行时可以选择它喜欢的任何一个。

您可以通过在另一个例程中运行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"}))
}

这总是返回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):
        // call timed out
        return false
    }
}

然后从循环中调用这个函数:

urls := []string{"url1", "url2", "url3"}
for _, url := range urls {
    go func() { fmt.Println(IsReachable(url)) }()
}

改变行

ch := make(chan bool, 1)

ch := make(chan bool)

您确实打开了一个异步(=非阻塞)通道,但您需要一个阻塞通道才能使其工作

这里返回的true结果在此场景中是确定的,它不是运行时随机选取的结果,因为只有true值可用(无论它需要多长时间才能变得可用!)被发送到通道中,false结果将永远不会在通道中可用。after()调用语句将永远不会有机会首先执行!

在这个select中,它看到的第一个可执行行是check(u)调用,而不是第一个case分支中的通道发送调用,或者任何其他调用!只有在第一次检查(u)执行返回之后,才会检查并调用选择分支用例,此时,true的值已经被推入第一个分支用例通道,所以这里没有通道阻塞到select语句,选择可以在这里迅速完成其目的,而无需检查剩余的分支用例!

所以看起来这里使用select在这个场景中似乎不太正确。

选择分支情况应该直接侦听通道发送和接收值,或者在必要时可选择使用默认值来逃避阻塞。

所以修复是一些人在这里已经指出,把长时间运行的任务或进程到一个单独的程序,并让它发送结果到通道,然后在主例程(或任何其他需要该通道值的例程)中,使用select分支用例侦听该特定通道上的值,或者侦听time.After(time.Second)调用提供的通道。

基本上,这行:case ch <- check(u)在将值发送到通道的意义上是正确的,但它只是不适合其预期用途(即阻塞该分支情况),因为case channel<-根本没有被阻塞(check(u)花费的时间都发生在通道参与之前),因为在一个单独的例程中,也就是主例程:return <-ch,它已经准备好读取该值了。这就是为什么在第二种情况下time.After()调用语句的分支在第一个实例中甚至没有机会被求值!

查看这个示例的简单解决方案,即。在单独的程序中正确使用select: https://gobyexample.com/timeouts

如果它有用,这里是@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
}

然后你可以调用'timeout'任何函数

if value,ok := WithTimeout(myFunc, time.Second); ok {
    // returned
} else {
    // didn't return
}

像这样调用来等待通道

if value,ok := WithTimeout(func()interface{}{return <- inbox}, time.Second); ok {
    // returned
} else {
    // didn't return
}

尝试发送

_,ok = WithTimeout(func()interface{}{outbox <- myValue; return nil}, time.Second)
    if !ok{...

最新更新