Goroutines/channelsを使用して、URLのリストに到達できるかどうかを確認しています。これが私のコードです。これは常にtrueを返すようです。タイムアウトのケースが実行されないのはなぜですか?目標は、URLの1つに到達できない場合でも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)
はcurrentゴルーチンでスリープします。つまり、func
を実行しています。 select
ステートメントは、一度戻ったときにのみ適切に実行され、その時点までに両方のブランチが実行可能であり、ランタイムはどちらでもブランチを選択できます。
もう1つの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の到達可能性を確認し、そのうちの1つが使用可能な場合はtrueを返したいようです。タイムアウトがゴルーチンを起動するのにかかる時間と比較して長い場合は、すべてのURLに対してタイムアウトを1つだけにすることで、これを簡略化できます。ただし、チャネルがすべてのチェックからの回答を保持するのに十分な大きさであることを確認する必要があります。そうしないと、「勝つ」ことができないものは永久にブロックされます。
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の到達可能性を並行して確認する場合は、コードを再構築する必要があります。
最初に、1つの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の値しかないため、ランタイムによって取得されるランダムな値ではありません。 time.After()呼び出しステートメントが最初から実行される機会を得ることはないので、time.After()呼び出しステートメント以降、誤った結果がチャネルで利用可能になることは決してありません!
この選択では、最初に表示される実行可能行はcheck(u)呼び出しであり、最初のケースブランチのチャネル送信呼び出しではなく、その他の呼び出しでもありません。そして、それは、この最初のcheck(u)の実行がここに戻った後でのみ、チェックされて呼び出されるブランチケースを選択します。その時点で、trueの値は最初のブランチケースチャネルにすでにプッシュされているため、チャネルブロッキングはありません。 selectステートメントに対して、selectは残りの分岐ケースを確認する必要なく、ここでその目的を迅速に果たすことができます!
このシナリオでは、ここではselectを使用しているため、正しくないようです。
選択ブランチのケースは、チャネルの送受信値を直接リッスンするか、必要に応じてオプションでデフォルトを使用してブロッキングを回避することになっています。
したがって、修正は一部の人々がすでにここで指摘したように、長時間実行されているタスクまたはプロセスを別のゴルーチンに入れ、結果をチャネルに送信してから、メインのゴルーチン(またはチャネルからその値を必要とする他のルーチン)に送信します)、selectブランチケースを使用して、特定のチャネルで値をリッスンするか、time.After(time.Second)呼び出しによって提供されるチャネルでリッスンします。
基本的に、この行:ケースch <-check(u)は、値をチャネルに送信するという意味では正しいですが、ケースチャネル<-はそうではないため、それは本来の用途(つまり、このブランチケースをブロックする)のためだけではありません。別のゴルーチンで、つまりメインのルーチンであるreturn <-chを使用するため、check(u)が費やす時間はすべて発生しています。押し出されます。そのため、2番目のケースブランチのtime.After()呼び出しステートメントは、最初のインスタンスでは評価される機会すらありません!
簡単な解決策については、この例を参照してください。個別のゴルーチンと組み合わせた選択の正しい使用: https://gobyexample.com/timeouts