Golang, используя таймауты с каналами

Я использую goroutines/channels, чтобы проверить, доступен ли список URL-адресов. Вот мой код. Кажется, что это всегда верно. Почему тайм-аут не выполняется? Цель состоит в том, чтобы вернуть false, даже если один из URL недоступен

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

check(u) будет спать в текущем goroutine, то есть в том, что работает func. Оператор select запускается корректно только после его возвращения, и к тому времени обе ветки выполняются, и среда выполнения может выбирать то, что ему нравится.

Вы можете решить это, запустив check внутри еще одного 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"}))
}

Кажется, вы хотите проверить доступность набора 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"}))
}

Ответ 2

Причина, по которой это всегда возвращает true, заключается в том, что вы вызываете check(u) в своем выражении select. Вам нужно вызвать его в подпрограмме 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)) }()
}

Play

Ответ 3

измените строку

ch := make(chan bool, 1)

to

ch := make(chan bool)

Вы открыли асинхронный (= неблокирующий) канал, но вам нужен канал блокировки, чтобы заставить его работать.

Ответ 4

Результат возврата истинного значения здесь является детерминированным в этом сценарии, а не случайным, полученным во время выполнения, потому что в канал отправляется только истинное значение (сколь бы долго оно не становилось доступным!), ложный результат никогда не будет доступен для канала, поскольку оператор вызова time.After() никогда не получит шанс быть выполненным в первую очередь!

В этом выборе первой исполняемой строкой, которую он видит, является вызов check (u), а не канал, отправляющий вызов в первом ответвлении, или любой другой вызов вообще! И только после того, как это первое выполнение check (u) вернулось сюда, будет выбран и проверен выбор вариантов ветвления, после чего значение true уже должно быть передано в канал первого варианта ветвления, так что здесь нет блокировки каналов к утверждению select, select может выполнить свое назначение здесь быстро, без необходимости проверять оставшиеся случаи ветвления!

Похоже на то, что использование select здесь не совсем корректно в этом сценарии.

Предполагается, что регистры выбора ветвей должны прослушивать отправляющие и получающие значения канала напрямую или, по желанию, по умолчанию, чтобы при необходимости избежать блокировки.

поэтому исправление, как уже отмечали некоторые люди, помещает долгосрочное задание или процесс в отдельную процедуру и позволяет отправить результат в канал, а затем в основной процедуре (или любой другой подпрограмме, которой требуется это значение вне канала), используйте регистры выбора ветки, чтобы либо прослушивать значение на этом конкретном канале, либо на канале, предоставленном time.After(time.Second ) позвоните.

По сути, эта строка: case ch & lt; - check (u) является верным в смысле отправки значения в канал, но это не только для его предполагаемого использования (то есть блокирует этот случай ветвления), потому что case channel & lt; - is не блокируется там вообще (проверка времени (u) тратится на все, что происходит до того, как канал включается), поскольку в отдельной программе, известной как главная: return & lt; -ch, он уже готов прочитать это значение всякий раз, когда это проталкивается. Вот почему оператор вызова time.After() во второй ветки case никогда не получит даже возможность быть оцененным, в первом случае!

см. этот пример для простого решения, т.е. правильное использование выбора в сочетании с отдельными программами: https://gobyexample.com/timeouts