¿Cómo comprobar que un canal está cerrado o no sin leerlo?

82

Este es un buen ejemplo del modo de trabajadores y controlador en Go escrito por @Jimt, en respuesta a " ¿Hay alguna forma elegante de pausar y reanudar cualquier otra rutina de gor en 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])
    }
}

Pero este código también tiene un problema: si desea eliminar un canal de trabajador workerscuando worker()sale, se produce un bloqueo.

Si usted close(workers[i]), la próxima vez que el controlador escriba en él, se producirá un pánico, ya que go no puede escribir en un canal cerrado. Si usa algún mutex para protegerlo, se bloqueará workers[i] <- Runningya workerque no lee nada del canal y la escritura se bloqueará, y el mutex provocará un bloqueo muerto. También puede dar un búfer más grande al canal como solución temporal, pero no es lo suficientemente bueno.

Así que creo que la mejor manera de resolver esto es worker()cerrar el canal cuando sale, si el controlador encuentra un canal cerrado, lo saltará y no hará nada. Pero no encuentro cómo comprobar que un canal ya está cerrado o no en esta situación. Si intento leer el canal en el controlador, es posible que el controlador esté bloqueado. Entonces estoy muy confundido por ahora.

PD: Recuperar el pánico levantado es lo que he intentado, pero cerrará la rutina que provocó el pánico. En este caso será el controlador, por lo que no sirve de nada.

Aún así, creo que es útil para el equipo de Go implementar esta función en la próxima versión de Go.

Reck Hou
fuente
¡Maneje el estado de su trabajador! Si cierra el canal, no es necesario volver a escribirlo.
jurka
Aquí, hice esto: github.com/atedja/go-tunnel .
atedja

Respuestas:

64

De una manera hacky, se puede hacer para los canales en los que se intenta escribir recuperando el pánico generado. Pero no puede verificar si un canal de lectura está cerrado sin leerlo.

O lo harás

  • eventualmente leerá el valor "verdadero" de él ( v <- c)
  • leer el valor "verdadero" y el indicador "no cerrado" ( v, ok <- c)
  • leer un valor cero y el indicador 'cerrado' ( v, ok <- c)
  • bloqueará en el canal leído para siempre ( v <- c)

Solo el último técnicamente no lee del canal, pero eso es de poca utilidad.

zzzz
fuente
1
Recuperar el pánico levantado es lo que he intentado, pero cerrará la rutina que provocó el pánico. En este caso será controllerasí que no sirve :)
Reck Hou
también puede escribir hack usando el paquete inseguro y
reflect,
78

No hay forma de escribir una aplicación segura en la que necesite saber si un canal está abierto sin interactuar con él.

La mejor manera de hacer lo que desea hacer es con dos canales: uno para el trabajo y otro para indicar el deseo de cambiar de estado (así como la finalización de ese cambio de estado si es importante).

Los canales son baratos. La semántica de sobrecarga de diseño complejo no lo es.

[además]

<-time.After(1e9)

es una forma realmente confusa y poco obvia de escribir

time.Sleep(time.Second)

Mantenga las cosas simples y todos (incluido usted) podrán entenderlas.

Dustin
fuente
7

Sé que esta respuesta es muy tarde, escribí esta solución, Hacking Go run-time , no es seguridad, puede fallar:

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
fuente
1
go vetdevuelve "posible uso indebido de unsafe.Pointer" en la última línea return *(*uint32)(unsafe.Pointer(cptr)) > 0y cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0))) hay una opción para hacerlo sin unsafe.Pointer en esas líneas?
Effi Bar-She'an
2
Necesita hacer toda la aritmética de punteros en una expresión para mantenerse feliz. Esta solución es una carrera de datos y no es válida para Go, también tendría que hacer como mínimo la lectura de cerrado con atomic.LoadUint32. Sin embargo, es un truco bastante frágil de cualquier manera, si hchan cambia entre las versiones de Go, esto se romperá.
Eloff
1
esto es probablemente muy inteligente, pero se siente como la adición de un problema en la parte superior de otro problema
Ярослав Рахматуллин
2

Bueno, se puede utilizar defaultla rama de detectarlo, por un canal cerrado serán seleccionados, por ejemplo: el siguiente código seleccionará default, channel, channel, el primer seleccionado no está bloqueado.

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

Huellas dactilares

2018/05/24 08:00:00 1.default
2018/05/24 08:00:01 2.channel
2018/05/24 08:00:01 3.channel
acrazar
fuente
3
Hay un problema con esta solución (así como con go101.org/article/channel-closing.html bastante bien escrito que propone una solución similar): no funciona si está utilizando un canal almacenado en búfer y contiene datos
Angad
@Angad Es cierto que esta no es la solución perfecta para detectar un canal cerrado. Es una solución perfecta para detectar si la lectura del canal se bloqueará. (es decir, si la lectura del canal se bloquea, entonces sabemos que no está cerrado; si la lectura del canal no se bloquea, sabemos que puede estar cerrado).
tombrown52
0

Puede establecer su canal en cero además de cerrarlo. De esa forma, puede comprobar si es nulo.

ejemplo en el patio de recreo: https://play.golang.org/p/v0f3d4DisCz

editar: Esta es en realidad una mala solución, como se demuestra en el siguiente ejemplo, porque configurar el canal en nil en una función lo rompería: https://play.golang.org/p/YVE2-LV9TOp

Kasper Gyselinck
fuente
pasar el canal por dirección (o en una estructura pasada por dirección)
ChuckCottrill
-1

De la documentación:

Un canal puede cerrarse con la función incorporada cerrar. El formulario de asignación de valores múltiples del operador de recepción informa si se envió un valor recibido antes de que se cerrara el canal.

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

El ejemplo de Golang en acción muestra este caso:

// 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
fuente
2
La cuestión era cómo comprobar el estado cerrado sin leer el canal, es decir, antes de escribir en él.
Peter
-5

es más fácil verificar primero si el canal tiene elementos, eso garantizaría que el canal esté activo.

func isChanClosed(ch chan interface{}) bool {
    if len(ch) == 0 {
        select {
        case _, ok := <-ch:
            return !ok
        }
    }
    return false 
}
Enric
fuente
3
Como mencionó Dustin , no hay forma de hacer esto de manera segura. Para cuando entres en tu ifcuerpo, len(ch)podría ser cualquier cosa. (por ejemplo, una goroutine en otro núcleo envía un valor al canal antes de que su selección intente leer).
Dave C
-7

Si escucha este canal, siempre podrá descubrir que el canal estaba cerrado.

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

Pero recuerde, no puede cerrar un canal dos veces. Esto aumentará el pánico.

jurka
fuente
5
Dije "sin leerlo", -1 por no leer la pregunta con atención.
Reck Hou
> PD: Recuperar el pánico levantado es lo que he intentado, pero cerrará la rutina que provocó el pánico. En este caso será el controlador, por lo que no sirve de nada. Siempre puedes ir a func (chan z) {diferir func () {// manejar recuperar} cerrar (z)}
jurka
Pero tengo que reservar el controlador y el close(z)trabajador me llamará en lugar del controlador.
Reck Hou