Go using timeouts with channels

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

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是否可达的逻辑,并根据实际情况返回truefalse。另外,你可以根据需要调整超时时间。

英文:

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 &quot;fmt&quot;
import &quot;time&quot;

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 &lt;- check(u):
			case &lt;-time.After(time.Second):
				ch&lt;-false
			}
		}(url)
	}
	return &lt;-ch
}
func main() {
	fmt.Println(IsReachable([]string{&quot;url1&quot;}))
}

答案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 &quot;fmt&quot;
import &quot;time&quot;

func check(u string, checked chan&lt;- bool) {
	time.Sleep(4 * time.Second)
	checked &lt;- 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 := &lt;-checked:
				ch &lt;- ret
			case &lt;-time.After(1 * time.Second):
				ch &lt;- false
			}
		}(url)
	}
	return &lt;-ch
}
func main() {
	fmt.Println(IsReachable([]string{&quot;url1&quot;}))
}

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 &quot;fmt&quot;
import &quot;time&quot;

func check(u string, ch chan&lt;- bool) {
	time.Sleep(4 * time.Second)
	ch &lt;- 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 &lt;- false })
	return &lt;-ch
}
func main() {
	fmt.Println(IsReachable([]string{&quot;url1&quot;, &quot;url2&quot;}))
}

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

Play

英文:

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 &lt;- check(url) }()
	select {
	case reachable := &lt;-ch:
		return reachable
	case &lt;-time.After(time.Second):
		// call timed out
		return false
	}
}

Then call this function from a loop:

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

Play

答案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 &lt;- delegate() }()
	select {
	case ret = &lt;-ch:
		return ret, true
	case &lt;-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&#39;t return
}

Call like this to wait for a channel

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

Like this to try sending

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

huangapple
  • 本文由 发表于 2014年5月10日 22:19:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/23582143.html
匿名

发表评论

匿名网友

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

确定