Jak sprawdzić, czy kanał jest zamknięty, czy nie, bez jego przeczytania?

82

To jest dobry przykład trybu pracowników i kontrolera w Go napisany przez @Jimt, w odpowiedzi na pytanie „ Czy istnieje jakiś elegancki sposób na wstrzymanie i wznowienie innych goroutine w golang?

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

// Possible worker states.
const (
    Stopped = 0
    Paused  = 1
    Running = 2
)

// Maximum number of workers.
const WorkerCount = 1000

func main() {
    // Launch workers.
    var wg sync.WaitGroup
    wg.Add(WorkerCount + 1)

    workers := make([]chan int, WorkerCount)
    for i := range workers {
        workers[i] = make(chan int)

        go func(i int) {
            worker(i, workers[i])
            wg.Done()
        }(i)
    }

    // Launch controller routine.
    go func() {
        controller(workers)
        wg.Done()
    }()

    // Wait for all goroutines to finish.
    wg.Wait()
}

func worker(id int, ws <-chan int) {
    state := Paused // Begin in the paused state.

    for {
        select {
        case state = <-ws:
            switch state {
            case Stopped:
                fmt.Printf("Worker %d: Stopped\n", id)
                return
            case Running:
                fmt.Printf("Worker %d: Running\n", id)
            case Paused:
                fmt.Printf("Worker %d: Paused\n", id)
            }

        default:
            // We use runtime.Gosched() to prevent a deadlock in this case.
            // It will not be needed of work is performed here which yields
            // to the scheduler.
            runtime.Gosched()

            if state == Paused {
                break
            }

            // Do actual work here.
        }
    }
}

// controller handles the current state of all workers. They can be
// instructed to be either running, paused or stopped entirely.
func controller(workers []chan int) {
    // Start workers
    for i := range workers {
        workers[i] <- Running
    }

    // Pause workers.
    <-time.After(1e9)
    for i := range workers {
        workers[i] <- Paused
    }

    // Unpause workers.
    <-time.After(1e9)
    for i := range workers {
        workers[i] <- Running
    }

    // Shutdown workers.
    <-time.After(1e9)
    for i := range workers {
        close(workers[i])
    }
}

Ale ten kod ma również problem: jeśli chcesz usunąć kanał roboczy w workersmomencie worker()wyjścia, nastąpi martwa blokada.

Jeśli ty close(workers[i]) , następnym razem, gdy kontroler zapisze do niego, wywoła panikę, ponieważ go nie może pisać do zamkniętego kanału. Jeśli użyjesz jakiegoś mutexa do ochrony, to utknie, workers[i] <- Runningponieważ workernie czyta niczego z kanału i zapis zostanie zablokowany, a mutex spowoduje martwą blokadę. Możesz również dać większy bufor do kanału jako obejście, ale to nie wystarczy.

Myślę więc, że najlepszym sposobem rozwiązania tego problemu jest worker()zamknięcie kanału przy wyjściu, jeśli sterownik znajdzie kanał zamknięty, przeskoczy nad nim i nic nie zrobi. Ale nie mogę znaleźć, jak sprawdzić, czy kanał jest już zamknięty, czy nie w tej sytuacji. Jeśli spróbuję odczytać kanał w kontrolerze, kontroler może być zablokowany. Więc na razie jestem bardzo zdezorientowany.

PS: Próbowałem odzyskać podniesioną panikę, ale zamknie to gorutynę, która wywołała panikę. W tym przypadku będzie to kontroler, więc nie ma sensu.

Mimo to myślę, że dla zespołu Go przydatne będzie zaimplementowanie tej funkcji w następnej wersji Go.

Reck Hou
źródło
Sprawdź stan swojego pracownika! Jeśli zamkniesz kanał, nie musisz ponownie do niego pisać.
jurka 19.04.13
Tutaj zrobiłem to: github.com/atedja/go-tunnel .
atedja

Odpowiedzi:

64

W hackerski sposób można to zrobić dla kanałów, do których próbuje się pisać, odzyskując wywołaną panikę. Ale nie możesz sprawdzić, czy kanał odczytu jest zamknięty bez czytania z niego.

Albo będziesz

  • ostatecznie odczytaj z niego „prawdziwą” wartość ( v <- c)
  • przeczytaj wartość „prawda” i wskaźnik „niezamknięte” ( v, ok <- c)
  • odczytaj wartość zerową i wskaźnik „zamknięte” ( v, ok <- c)
  • zablokuje się w kanale czytać na zawsze ( v <- c)

Tylko ten ostatni technicznie nie czyta z kanału, ale to mało przydatne.

zzzz
źródło
1
Próbowałem odzyskać podniesioną panikę, ale zamknie to gorutynę, która wywołała panikę. W tym przypadku będzie controllerto bez sensu :)
Reck Hou
możesz również napisać hack używając niebezpiecznego i odzwierciedlać pakiet zobacz moją odpowiedź
youssif
78

Nie ma sposobu, aby napisać bezpieczną aplikację, w której musisz wiedzieć, czy kanał jest otwarty, bez interakcji z nim.

Najlepszym sposobem na zrobienie tego, co chcesz, są dwa kanały - jeden do pracy, a drugi do wskazania chęci zmiany stanu (a także zakończenia zmiany tego stanu, jeśli jest to ważne).

Kanały są tanie. Złożona semantyka przeciążania projektu nie jest.

[również]

<-time.After(1e9)

to naprawdę zagmatwany i nieoczywisty sposób pisania

time.Sleep(time.Second)

Postaraj się, aby wszystko było proste, a każdy (w tym Ty) może je zrozumieć.

Dustin
źródło
7

Wiem, że ta odpowiedź jest tak późna, napisałem to rozwiązanie, Hacking Go run-time , To nie jest bezpieczeństwo, może się zawiesić:

import (
    "unsafe"
    "reflect"
)


func isChanClosed(ch interface{}) bool {
    if reflect.TypeOf(ch).Kind() != reflect.Chan {
        panic("only channels!")
    }
    
    // get interface value pointer, from cgo_export 
    // typedef struct { void *t; void *v; } GoInterface;
    // then get channel real pointer
    cptr := *(*uintptr)(unsafe.Pointer(
        unsafe.Pointer(uintptr(unsafe.Pointer(&ch)) + unsafe.Sizeof(uint(0))),
    ))
    
    // this function will return true if chan.closed > 0
    // see hchan on https://github.com/golang/go/blob/master/src/runtime/chan.go 
    // type hchan struct {
    // qcount   uint           // total data in the queue
    // dataqsiz uint           // size of the circular queue
    // buf      unsafe.Pointer // points to an array of dataqsiz elements
    // elemsize uint16
    // closed   uint32
    // **
    
    cptr += unsafe.Sizeof(uint(0))*2
    cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0)))
    cptr += unsafe.Sizeof(uint16(0))
    return *(*uint32)(unsafe.Pointer(cptr)) > 0
}
youssif
źródło
1
go vetzwraca „możliwe niewłaściwe użycie unsafe.Pointer” w ostatnim wierszu return *(*uint32)(unsafe.Pointer(cptr)) > 0i cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0))) czy istnieje możliwość zrobienia tego bez pliku unsafe.Pointer w tych wierszach?
Effi Bar-She'an
2
Musisz wykonać wszystkie obliczenia wskaźników w jednym wyrażeniu, aby być szczęśliwym weterynarzem. To rozwiązanie jest wyścigiem danych i nie jest poprawne w Go, musiałbyś również co najmniej odczytać zamkniętą za pomocą atomic.LoadUint32. Tak czy inaczej jest to dość delikatny hack, jeśli hchan zmieni się między wersjami Go, to się zepsuje.
Eloff
1
jest to prawdopodobnie bardzo sprytne, ale wydaje się, że dodanie problemu do innego problemu
Ярослав Рахматуллин
2

Cóż, można użyć defaultoddział wykryć go do zamkniętego kanału zostanie wybrany, na przykład: poniższy kod wybierze default, channel, channel, pierwszy wybór nie jest zablokowany.

func main() {
    ch := make(chan int)

    go func() {
        select {
        case <-ch:
            log.Printf("1.channel")
        default:
            log.Printf("1.default")
        }
        select {
        case <-ch:
            log.Printf("2.channel")
        }
        close(ch)
        select {
        case <-ch:
            log.Printf("3.channel")
        default:
            log.Printf("3.default")
        }
    }()
    time.Sleep(time.Second)
    ch <- 1
    time.Sleep(time.Second)
}

Wydruki

2018/05/24 08:00:00 1.default
2018/05/24 08:00:01 2.channel
2018/05/24 08:00:01 3.channel
drażnienie
źródło
3
Jest jeden problem z tym rozwiązaniem (a także dość ładnie napisanym go101.org/article/channel-closing.html, który proponuje podobne rozwiązanie) - nie działa, jeśli używasz kanału buforowanego i zawiera nieprzeczytane dane
Angad,
@Angad To prawda, że ​​nie jest to idealne rozwiązanie do wykrywania zamkniętego kanału. Jest to idealne rozwiązanie do wykrywania, czy odczyt kanału będzie blokowany. (tj. jeśli odczyt kanału się zablokuje, to wiemy, że nie jest zamknięty; jeśli odczyt kanału nie zablokuje się, wiemy, że może być zamknięty).
tombrown52
0

Oprócz zamknięcia kanału możesz ustawić wartość zero. W ten sposób możesz sprawdzić, czy jest zero.

przykład na placu zabaw: https://play.golang.org/p/v0f3d4DisCz

edycja: jest to właściwie złe rozwiązanie, jak pokazano w następnym przykładzie, ponieważ ustawienie kanału na zero w funkcji spowodowałoby jej uszkodzenie : https://play.golang.org/p/YVE2-LV9TOp

Kasper Gyselinck
źródło
przekazanie kanału przez adres (lub w strukturze przekazanej przez adres)
ChuckCottrill
-1

Z dokumentacji:

Kanał można zamknąć funkcją wbudowaną close. Formularz przypisania wielowartościowego operatora odbioru informuje, czy odebrana wartość została wysłana przed zamknięciem kanału.

https://golang.org/ref/spec#Receive_operator

Przykład Golang in Action pokazuje ten przypadek:

// This sample program demonstrates how to use an unbuffered
// channel to simulate a game of tennis between two goroutines.
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// wg is used to wait for the program to finish.
var wg sync.WaitGroup

func init() {
    rand.Seed(time.Now().UnixNano())
}

// main is the entry point for all Go programs.
func main() {
    // Create an unbuffered channel.
    court := make(chan int)
    // Add a count of two, one for each goroutine.
    wg.Add(2)
    // Launch two players.
    go player("Nadal", court)
    go player("Djokovic", court)
    // Start the set.
    court <- 1
    // Wait for the game to finish.
    wg.Wait()
}

// player simulates a person playing the game of tennis.
func player(name string, court chan int) {
    // Schedule the call to Done to tell main we are done.
    defer wg.Done()
    for {
        // Wait for the ball to be hit back to us.
        ball, ok := <-court
        fmt.Printf("ok %t\n", ok)
        if !ok {
            // If the channel was closed we won.
            fmt.Printf("Player %s Won\n", name)
            return
        }
        // Pick a random number and see if we miss the ball.
        n := rand.Intn(100)
        if n%13 == 0 {
            fmt.Printf("Player %s Missed\n", name)
            // Close the channel to signal we lost.
            close(court)
            return
        }

        // Display and then increment the hit count by one.
        fmt.Printf("Player %s Hit %d\n", name, ball)
        ball++
        // Hit the ball back to the opposing player.
        court <- ball
    }
}
Israel Barba
źródło
2
Pytanie brzmiało, jak sprawdzić stan zamknięty bez odczytu kanału, czyli przed zapisaniem do niego.
Peter
-5

łatwiej jest najpierw sprawdzić, czy kanał ma elementy, które zapewniłyby, że kanał żyje.

func isChanClosed(ch chan interface{}) bool {
    if len(ch) == 0 {
        select {
        case _, ok := <-ch:
            return !ok
        }
    }
    return false 
}
Enric
źródło
3
Jak wspomniał Dustin , nie ma sposobu, aby to zrobić bezpiecznie. Zanim wejdziesz do swojego ifciała, len(ch)może być wszystko. (np. goroutine na innym rdzeniu wysyła wartość do kanału, zanim wybrany spróbuje odczytać).
Dave C,
-7

Jeśli słuchasz tego kanału, zawsze możesz dowiedzieć się, że kanał został zamknięty.

case state, opened := <-ws:
    if !opened {
         // channel was closed 
         // return or made some final work
    }
    switch state {
        case Stopped:

Pamiętaj jednak, że nie możesz zamknąć jednego kanału dwa razy. To wzbudzi panikę.

jurka
źródło
5
Powiedziałem „bez czytania”, -1, żeby nie czytać dokładnie pytania.
Reck Hou
> PS: Próbowałem odzyskać podniesioną panikę, ale to zamknie gorutynę, która wywołała panikę. W tym przypadku będzie to kontroler, więc nie ma sensu. Zawsze możesz iść func (chan z) {defer func () {// uchwyt odzyskać} close (z)}
jurka
Ale muszę zarezerwować kontroler i close(z)będę wywoływany przez pracownika zamiast kontrolera.
Reck Hou