104 votes

Comment vérifier qu'un canal est fermé ou non sans le lire ?

Voici un bon exemple de mode workers & controller en Go écrit par @Jimt, en réponse à la question suivante " Existe-t-il un moyen élégant de mettre en pause et de reprendre n'importe quelle autre goroutine dans 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])
    }
}

Mais ce code a aussi un problème : Si vous voulez supprimer un canal de travailleur dans workers cuando worker() sort, un verrou mort se produit.

Si usted close(workers[i]) La prochaine fois qu'un contrôleur y écrit, cela provoque une panique car il ne peut pas écrire dans un canal fermé. Si vous utilisez un mutex pour le protéger, alors il sera bloqué sur workers[i] <- Running depuis le worker ne lit rien depuis le canal et l'écriture sera bloquée, et le mutex causera un verrou mort. Vous pouvez aussi donner un plus grand tampon au canal comme solution de contournement, mais ce n'est pas suffisant.

Donc je pense que la meilleure façon de résoudre ce problème est de worker() fermer le canal à la sortie, si le contrôleur trouve un canal fermé, il sautera par-dessus et ne fera rien. Mais je ne trouve pas comment vérifier si un canal est déjà fermé ou non dans cette situation. Si j'essaie de lire le canal dans le contrôleur, le contrôleur pourrait être bloqué. Je suis donc très confus pour le moment.

PS : J'ai essayé de récupérer la panique soulevée, mais cela fermera la goroutine qui a soulevé la panique. Dans ce cas, il s'agira d'un contrôleur, donc ce n'est pas utile.

Néanmoins, je pense qu'il est utile pour l'équipe de Go d'implémenter cette fonction dans la prochaine version de Go.

88voto

Dustin Points 35205

Il n'y a aucun moyen d'écrire une application sûre où vous devez savoir si un canal est ouvert sans interagir avec lui.

La meilleure façon de faire ce que vous voulez faire est d'utiliser deux canaux - un pour le travail et un pour indiquer le désir de changer d'état (ainsi que l'achèvement de ce changement d'état si c'est important).

Les chaînes sont bon marché. La sémantique de surcharge de la conception complexe ne l'est pas.

[également]

<-time.After(1e9)

est une façon vraiment confuse et non évidente d'écrire

time.Sleep(time.Second)

Faites en sorte que les choses soient simples et que tout le monde (y compris vous) puisse les comprendre.

85voto

zzzz Points 23017

De façon fantaisiste, cela peut être fait pour les canaux sur lesquels on tente d'écrire en récupérant la panique soulevée. Mais vous ne pouvez pas vérifier si un canal de lecture est fermé sans y lire.

Soit vous

  • éventuellement lire la "vraie" valeur de celui-ci ( v <- c )
  • lire la valeur "vraie" et l'indicateur "non fermé" ( v, ok <- c )
  • lire un valeur zéro et l'indicateur "fermé" ( v, ok <- c ) ( exemple )
  • bloquera dans le canal lu pour toujours ( v <- c )

Seul le dernier ne lit pas techniquement le canal, mais cela n'est pas très utile.

8voto

Je sais que cette réponse est très tardive, j'ai écrit cette solution, Hacking Aller à l'exécution Ce n'est pas de la sécurité, ça peut s'écraser :

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
}

3voto

kcats Points 489

Eh bien, vous pouvez utiliser default pour le détecter, pour un canal fermé sera sélectionné, par exemple : le code suivant sélectionnera default , channel , channel la première sélection n'est pas bloquée.

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

Imprimés

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

Note, se référer au commentaire de @Angad sous cette réponse :

Cela ne fonctionne pas si vous utilisez un canal tamponné et qu'il contient des données non lues

2voto

jregovic Points 29

J'ai souvent rencontré ce problème avec des goroutines multiples et simultanées.

C'est peut-être ou non un bon modèle, mais je définis une structure pour mes travailleurs avec un canal de sortie et un champ pour l'état du travailleur :

type Worker struct {
    data chan struct
    quit chan bool
    stopped bool
}

Ensuite, vous pouvez demander à un contrôleur d'appeler une fonction d'arrêt pour le travailleur :

func (w *Worker) Stop() {
    w.quit <- true
    w.stopped = true
}

func (w *Worker) eventloop() {
    for {
        if w.Stopped {
            return
        }
        select {
            case d := <-w.data:
                //DO something
                if w.Stopped {
                    return
                }
            case <-w.quit:
                return
        }
    }
}

Cela vous donne un bon moyen d'obtenir un arrêt net de vos travailleurs sans que rien ne soit suspendu ou ne génère d'erreurs, ce qui est particulièrement utile lors de l'exécution dans un conteneur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X