Wie kann ich überprüfen, ob ein Kanal geschlossen ist oder nicht, ohne ihn zu lesen?


82

Dies ist ein gutes Beispiel für den Worker & Controller-Modus in Go, geschrieben von @Jimt, als Antwort auf " Gibt es eine elegante Möglichkeit, eine andere Goroutine in Golang anzuhalten und fortzusetzen? "

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])
    }
}

Dieser Code hat jedoch auch ein Problem: Wenn Sie einen Worker-Kanal in entfernen möchten workers , wenn worker()Ausfahrten, Tot - Schloss geschieht.

Wenn Sie das close(workers[i])nächste Mal in den Controller schreiben, wird eine Panik ausgelöst, da go nicht in einen geschlossenen Kanal schreiben kann. Wenn Sie zum Schutz einen Mutex verwenden, bleibt dieser haftenworkers[i] <- Running da workerder Kanal nichts liest und das Schreiben blockiert wird und der Mutex eine Deadlock-Funktion verursacht. Sie können dem Kanal auch einen größeren Puffer als Workaround geben, aber er ist nicht gut genug.

Ich denke, der beste Weg, dies zu lösen, ist worker() Schließen des Kanals beim Beenden. Wenn der Controller einen geschlossenen Kanal findet, springt er darüber und tut nichts. Ich kann jedoch nicht feststellen, wie überprüft werden kann, ob ein Kanal in dieser Situation bereits geschlossen ist oder nicht. Wenn ich versuche, den Kanal im Controller zu lesen, ist der Controller möglicherweise blockiert. Ich bin also vorerst sehr verwirrt.

PS: Ich habe versucht, die erhöhte Panik wiederherzustellen, aber sie wird die Goroutine schließen, die die Panik ausgelöst hat. In diesem Fall handelt es sich um einen Controller, der keinen Nutzen hat.

Dennoch denke ich, dass es für das Go-Team nützlich ist, diese Funktion in der nächsten Version von Go zu implementieren.


Behandeln Sie den Zustand Ihres Arbeiters! Wenn Sie den Kanal schließen, müssen Sie nicht erneut darauf schreiben.
Jurka

Hier habe ich folgendes gemacht: github.com/atedja/go-tunnel .
Atedja

Antworten:


63

Auf hackige Weise kann dies für Kanäle geschehen, auf die man zu schreiben versucht, indem man die erhöhte Panik wieder herstellt. Sie können jedoch nicht überprüfen, ob ein Lesekanal geschlossen ist, ohne daraus zu lesen.

Entweder du wirst

  • Lesen Sie schließlich den "wahren" Wert daraus (v <- c )
  • Lesen Sie den Wert "true" und die Anzeige "nicht geschlossen" (v, ok <- c )
  • Lesen Sie einen Nullwert und die Anzeige 'geschlossen' (v, ok <- c )
  • blockiert im Kanal für immer lesen ( v <- c)

Nur der letzte liest technisch gesehen nicht aus dem Kanal, aber das nützt wenig.


1
Ich habe versucht, die erhöhte Panik wiederherzustellen, aber sie wird die Goroutine schließen, die die Panik ausgelöst hat. In diesem Fall wird es controllerso sein, dass es keinen Sinn macht :)
Reck Hou

Sie können auch Hack mit unsicheren und reflektierenden Paket schreiben, siehe meine Antwort
youssif

76

Es gibt keine Möglichkeit, eine sichere Anwendung zu schreiben, in der Sie wissen müssen, ob ein Kanal geöffnet ist, ohne mit ihm zu interagieren.

Der beste Weg, um das zu tun, was Sie tun möchten, sind zwei Kanäle - einer für die Arbeit und einer, um den Wunsch anzuzeigen, den Status zu ändern (sowie den Abschluss dieser Statusänderung, wenn dies wichtig ist).

Kanäle sind billig. Komplexe Designüberladungssemantik ist nicht.

[ebenfalls]

<-time.After(1e9)

ist eine wirklich verwirrende und nicht offensichtliche Art zu schreiben

time.Sleep(time.Second)

Halten Sie die Dinge einfach und jeder (einschließlich Sie) kann sie verstehen.


8

Ich weiß, dass diese Antwort so spät ist, ich habe diese Lösung geschrieben, Hacking Go-Laufzeit , Es ist keine Sicherheit, Es kann abstürzen:

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
}

1
go vetreturns „möglichen Missbrauch von unsafe.Pointer“ auf der letzten Zeile return *(*uint32)(unsafe.Pointer(cptr)) > 0und cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0))) gibt es eine Option, ohne unsafe.Pointer in diese Richtung zu tun?
Effi Bar-She'an

1
Sie müssen alle Zeigerarithmetik in einem Ausdruck ausführen, um den Tierarzt bei Laune zu halten. Diese Lösung ist ein Datenrennen und nicht gültig. Sie müssten außerdem mindestens das Lesen von geschlossen mit atomic.LoadUint32 durchführen. Es ist jedoch ein ziemlich fragiler Hack, wenn hchan zwischen Go-Versionen wechselt, wird dies kaputt gehen.
Eloff

1
Das ist wahrscheinlich sehr klug, aber es fühlt sich an, als würde man ein Problem zusätzlich zu einem anderen Problem hinzufügen
23рослав Рахматуллин

3

Nun, können Sie den defaultZweig zu erfassen, für einen geschlossenen Kanal ausgewählt werden, zum Beispiel: der folgende Code wählt default, channel, channeldie erste Auswahl wird nicht blockiert.

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)
}

Druckt

2018/05/24 08:00:00 1.default
2018/05/24 08:00:01 2.channel
2018/05/24 08:00:01 3.channel

3
Es gibt ein Problem mit dieser Lösung (sowie die ziemlich gut geschriebene go101.org/article/channel-closing.html , die eine ähnliche Lösung vorschlägt) - sie funktioniert nicht, wenn Sie einen gepufferten Kanal verwenden und sie enthält ungelesene Daten
Angad

@Angad Es ist wahr, dass dies keine perfekte Lösung ist, um einen geschlossenen Kanal zu erkennen. Es ist eine perfekte Lösung, um festzustellen, ob das Lesen des Kanals blockiert. (dh wenn das Lesen des Kanals blockiert wird, wissen wir, dass er nicht geschlossen ist; wenn das Lesen des Kanals nicht blockiert wird, wissen wir, dass er möglicherweise geschlossen ist).
tombrown52

0

Aus der Dokumentation:

Ein Kanal kann mit der eingebauten Funktion schließen geschlossen werden. Das mehrwertige Zuweisungsformular des Empfangsoperators gibt an, ob ein empfangener Wert gesendet wurde, bevor der Kanal geschlossen wurde.

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

Ein Beispiel von Golang in Aktion zeigt diesen Fall:

// 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
    }
}

2
Die Frage war, wie man auf geschlossenen Zustand prüft, ohne den Kanal zu lesen, dh bevor man darauf schreibt.
Peter


-5

Es ist einfacher, zuerst zu überprüfen, ob der Kanal Elemente enthält, die sicherstellen, dass der Kanal lebt.

func isChanClosed(ch chan interface{}) bool {
    if len(ch) == 0 {
        select {
        case _, ok := <-ch:
            return !ok
        }
    }
    return false 
}

3
Wie von Dustin erwähnt , gibt es keine Möglichkeit, dies sicher zu tun. Bis du in deinen ifKörper kommst, len(ch)könnte alles sein. (zB sendet eine Goroutine auf einem anderen Kern einen Wert an den Kanal, bevor Ihre Auswahl versucht zu lesen).
Dave C

-8

Wenn Sie diesen Kanal hören, können Sie immer feststellen, dass dieser Kanal geschlossen wurde.

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

Denken Sie jedoch daran, dass Sie einen Kanal nicht zweimal schließen können. Dies wird Panik auslösen.


5
Ich sagte "ohne es zu lesen", -1 für nicht sorgfältig gelesene Frage.
Reck Hou

> PS: Ich habe versucht, die erhöhte Panik wiederherzustellen, aber es wird die Goroutine schließen, die die Panik ausgelöst hat. In diesem Fall handelt es sich um einen Controller, sodass er keinen Nutzen hat. Sie können immer func (chan z) {defer func () {// behandeln wiederherstellen} schließen (z)}
jurka

Aber ich muss den Controller reservieren und close(z)werde vom Arbeiter anstelle des Controllers angerufen.
Reck Hou
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.