web-dev-qa-db-fra.com

golang: Comment la sélection a-t-elle fonctionné lorsque plusieurs chaînes ont été impliquées?

J'ai trouvé lors de l'utilisation de select sur plusieurs canaux non mis en tampon, comme

select {
case <- chana:
case <- chanb:
}

Même lorsque les deux canaux ont des données, mais lors du traitement de cette sélection, l'appel qui tombe dans le cas où chana et le cas chanb n'est pas équilibré.

package main

import (
    "fmt"
    _ "net/http/pprof"
    "sync"
    "time"
)

func main() {
    chana := make(chan int)
    chanb := make(chan int)

    go func() {
        for i := 0; i < 1000; i++ {
            chana <- 100 * i
        }
    }()

    go func() {
        for i := 0; i < 1000; i++ {
            chanb <- i
        }
    }()

    time.Sleep(time.Microsecond * 300)

    acount := 0
    bcount := 0
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
        for {
            select {
            case <-chana:
                acount++
            case <-chanb:
                bcount++
            }
            if acount == 1000 || bcount == 1000 {
                fmt.Println("finish one acount, bcount", acount, bcount)
                break
            }
        }
        wg.Done()
    }()

    wg.Wait()
}

Exécutez cette démo, lorsque l’un des chana, chanb a terminé la lecture/écriture, l’autre peut rester 999-1 à gauche.

Existe-t-il une méthode pour assurer l'équilibre?

trouvé sujet connexe
golang-channels-select-statement

5
Terry Pang

La déclaration Go select n'est pas biaisée en faveur des cas (prêts). Citant de la spécification:

Si une ou plusieurs communications peuvent être établies, une seule pouvant continuer est sélectionnée via une sélection pseudo-aléatoire uniforme. Sinon, s'il existe un cas par défaut, ce cas est choisi. S'il n'y a pas de cas par défaut, l'instruction "select" est bloquée jusqu'à ce qu'au moins une des communications puisse continuer.

Si plusieurs communications peuvent continuer, une est sélectionnée au hasard. Ce n'est pas une distribution aléatoire parfaite, et la spécification ne le garantit pas, mais c'est aléatoire.

Ce que vous ressentez résulte du fait que le Go Playground a GOMAXPROCS=1 ( que vous pouvez vérifier ici ) et que le planificateur de goroutine n’est pas préemptif. Cela signifie que par défaut les goroutines ne sont pas exécutées en parallèle. Un goroutine est mis en attente si une opération de blocage est rencontrée (par exemple, en lisant sur le réseau, ou en essayant de recevoir ou d’envoyer sur un canal bloquant), et un autre prêt à fonctionner continue.

Et comme il n'y a pas d'opération de blocage dans votre code, les goroutines ne peuvent pas être mises en parc, et il se peut qu'un seul de vos goroutines "producteurs" s'exécute et que l'autre ne soit pas planifié (jamais).

En exécutant votre code sur mon ordinateur local où GOMAXPROCS=4, j’ai des résultats très "réalistes". En l'exécutant quelques fois, la sortie:

finish one acount, bcount 1000 901
finish one acount, bcount 1000 335
finish one acount, bcount 1000 872
finish one acount, bcount 427 1000

Si vous avez besoin de donner la priorité à un seul cas, vérifiez cette réponse: Forcer la priorité de l'instruction go select

Le comportement par défaut de select ne garantit pas une priorité égale, mais en moyenne, il en sera proche. Si vous avez besoin d'une priorité égale garantie, vous ne devriez pas utiliser select, mais vous pouvez créer une séquence de 2 réceptions non bloquantes provenant des 2 canaux, qui pourrait ressembler à ceci:

for {
    select {
    case <-chana:
        acount++
    default:
    }
    select {
    case <-chanb:
        bcount++
    default:
    }
    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

La réception non bloquante ci-dessus 2 drainera les 2 canaux à vitesse égale (avec une priorité égale) si les deux valeurs d'alimentation sont fournies et si l'une ne le fait pas, l'autre est constamment reçue de l'autre sans être retardée ou bloquée.

Une chose à noter à ce sujet est que si aucun des canaux fournissent les valeurs à recevoir, il s'agira essentiellement d'une boucle "occupée" et consommera par conséquent de la puissance de calcul. Pour éviter cela, nous pouvons détecter qu'aucun des canaux n'était prêt, et alors utiliser une instruction select avec les deux destinataires, qui se bloqueront ensuite jusqu'à ce que l'un d'eux soit prêt à recevoir de Ressources du processeur:

for {
    received := 0
    select {
    case <-chana:
        acount++
        received++
    default:
    }
    select {
    case <-chanb:
        bcount++
        received++
    default:
    }

    if received == 0 {
        select {
        case <-chana:
            acount++
        case <-chanb:
            bcount++
        }
    }

    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

Pour plus de détails sur la planification de goroutine, consultez ces questions:

Nombre de threads utilisés par le runtime Go

Goroutines 8kb et Windows OS thread 1 mb

Pourquoi ne crée-t-il pas beaucoup de threads quand beaucoup de goroutines sont bloquées lors de l'écriture de fichier dans golang?

9
icza

Comme mentionné dans le commentaire, si vous voulez assurer l'équilibre, vous pouvez simplement renoncer à utiliser select dans le goroutine de lecture et vous appuyer sur la synchronisation fournie par les canaux sans tampon:

go func() {
    for {
        <-chana
        acount++
        <-chanb
        bcount++

        if acount == 1000 || bcount == 1000 {
            fmt.Println("finish one acount, bcount", acount, bcount)
            break
        }
    }
    wg.Done()
}()
2
Matt Harrison

Edited : Vous pouvez également trouver un équilibre du côté de l'offre, mais la réponse de @ icza semble être une meilleure option que celle-ci et explique également la planification qui a été à l'origine de cette situation. Étonnamment, c'était unilatéral, même sur ma machine (virtuelle).

Voici quelque chose qui peut équilibrer deux routines du côté de l'offre (d'une manière ou d'une autre ne semble pas fonctionner sur Playground). 

package main

import (
    "fmt"
    _ "net/http/pprof"
    "sync"
    "sync/atomic"
    "time"
)

func main() {
    chana := make(chan int)
    chanb := make(chan int)
    var balanceSwitch int32

    go func() {
        for i := 0; i < 1000; i++ {
            for atomic.LoadInt32(&balanceSwitch) != 0 {
                fmt.Println("Holding R1")
                time.Sleep(time.Nanosecond * 1)
            }
            chana <- 100 * i
            fmt.Println("R1: Sent i", i)
            atomic.StoreInt32(&balanceSwitch, 1)

        }
    }()

    go func() {
        for i := 0; i < 1000; i++ {

            for atomic.LoadInt32(&balanceSwitch) != 1 {
                fmt.Println("Holding R2")
                time.Sleep(time.Nanosecond * 1)
            }
            chanb <- i
            fmt.Println("R2: Sent i", i)
            atomic.StoreInt32(&balanceSwitch, 0)

        }
    }()

    time.Sleep(time.Microsecond * 300)

    acount := 0
    bcount := 0
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
        for {
            select {
            case <-chana:
                acount++
            case <-chanb:
                bcount++
            }
            fmt.Println("Acount Bcount", acount, bcount)
            if acount == 1000 || bcount == 1000 {
                fmt.Println("finish one acount, bcount", acount, bcount)
                break
            }
        }
        wg.Done()
    }()

    wg.Wait()
}

En modifiant atomic.LoadInt32(&balanceSwitch) != XX et atomic.StoreInt32(&balanceSwitch, X), ou d'autres mécanismes, vous pouvez le mapper sur un nombre quelconque de routines. Ce n'est peut-être pas la meilleure chose à faire, mais si c'est une exigence, vous devrez peut-être envisager de telles options. J'espère que cela t'aides.

0
Ravi