Les channels dans le langage de programmation Go

Ce chapitre vous explique les channels en GoLang. Ils permettent de créer, connecter, synchroniser et communiquer les différentes goroutines. Vous allez apprendre à créer, envoyer, recevoir vos channels et à gérer les différents cas d'erreurs des channels dans le langage de programmation GO.

C'est quoi les channels ?

Dans ce tutoriel nous allons parler des channels. Les channels sont utilisés avec les goroutines pour envoyer des données (int, string, struct…) d'une goroutine et les recevoir dans une autre goroutine. C'est un moyen de connecter les différentes goroutines, c'est un moyen de communication et de synchronisation entre les goroutines. La transmission des channels se fait qu'avec des goroutines

Les channels

Déclarer un channel

Pour déclarer votre channel, vous utiliserez le mot-clé make avec le mot-clé chan suivit du type de donnée que vous souhaitez transiter.

ch := make(chan typeDeValeur)

Envoyer et récupérer un channel

Pour envoyer ou recevoir une valeur dans un channel, il faut utiliser l'opérateur <-.

Exemple :

package main

import "fmt"

func run(c chan string, name string) {
    c <- name // envoyer une valeur d'un channel
}

func main() {
    canal := make(chan string)
    go run(canal, "Hatim")
    fmt.Println(<-canal) // récupérer une valeur d'un channel
}

Résultat :

Hatim

Les channels sont bloquants

Les envois et les réceptions sur un channel est bloquant par défaut. Qu'est-ce que ça veut dire ? Lorsqu'une donnée est envoyée à un channel, le contrôle est bloqué dans l'instruction d'envoi jusqu'à ce qu'une autre goroutine lise depuis ce channel. De la même manière, lorsque des données sont lues sur un channel, la lecture est bloquée jusqu'à ce qu'une certaine goroutine écrit des données sur ce channel.

C'est cette propriété des channels qui permet aux goroutines de communiquer efficacement sans l'utilisation de verrous explicites ou de variables conditionnelles.

package main

import (
	"fmt"
	"time"
)

func run(ch chan string, name string) {
	time.Sleep(time.Second * 2)
	fmt.Println("fonction run() :", name)
	ch <- name
}

func main() {

	now := time.Now()

	ch := make(chan string)

	go run(ch, "channel 1")
	fmt.Println("fonction main() :", <-ch)

	go run(ch, "channel 2")
	fmt.Println("fonction main() :", <-ch)

	fmt.Println(time.Now().Sub(now))
}

Résultat :

fonction run() : channel 1
fonction main() : channel 1
fonction run() : channel 2
fonction main() : channel 2
4.00097503s

Nous avons lancé 2 fois la goroutine de la fonction run() avec une lecture sur le channel ch, nous pouvons remarquer que le temps d’exécution est de 4 secondes avec un intervalle de 2 secondes entre chaque goroutine, ce qui prouve que les channels sont bien bloquants.

Par contre ici on ne profite pas forcément de la puissance des goroutines car elles ne sont pas lancées en simultanée vu que le channel est placé entre chaque goroutine. Pour régler ce problème je vais changer l'ordre de lecture de nos channels.

package main

import (
    "fmt"
    "time"
)

func run(ch chan string, name string) {
    time.Sleep(time.Second * 2)
    fmt.Println("fonction run() :", name)
    ch <- name
}

func main() {

    now := time.Now()

    ch := make(chan string)

    go run(ch, "channel 1")
	go run(ch, "channel 2")
	
	// changement d'ordre de lecture de nos channels
    fmt.Println("fonction main() :", <-ch)
    fmt.Println("fonction main() :", <-ch)

    fmt.Println(time.Now().Sub(now))
}

Résultat :

fonction run() : channel 1
fonction main() : channel 1
fonction run() : channel 2
fonction main() : channel 2
2.000341006s

Information

La première goroutine qui aura écrit sur un channel sera la première à être lu dans notre programme.

deadlock

Par défaut, les channels sont dit unbuffered, ce qui signifie qu'ils n'accepteront pas de récepteur ( chan<-) que s'il existe un expéditeur ( <- chan) correspondant prêt à recevoir la valeur envoyée, l'inverse est aussi vrai.

Voila ce que j'entend par expéditeur et récepteur :

  • récepteur : c'est le moment où on entre une valeur dans notre channel
  • expéditeur : c'est le moment où on lit une valeur depuis notre channel

Dans cet exemple je vais créer un channel avec 5 récepteurs et 5 expéditeurs :

package main

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

var wg = sync.WaitGroup{}

func main() {

    now := time.Now()
    ch := make(chan int)

    // 5 expéditeurs
    for j := 0; j < 5; j++ {
        wg.Add(1)
        go func() {
            time.Sleep(time.Second * 2)
            i := <-ch
            fmt.Println(i)
            wg.Done()
        }()
    }

    // 5 récepteurs
    for j := 0; j < 5; j++ {
        wg.Add(1)
        go func() {
            time.Sleep(time.Second * 2)
            ch <- 50

            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println(time.Now().Sub(now))
}

Résultat :

50
50
50
50
50
2.000827906s

Ici nous avons autant de récepteurs que d'expéditeurs, donc aucune erreur dans notre programme, mais maintenant je vais rajouter un récepteur en plus.

package main

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

var wg = sync.WaitGroup{}

func main() {

    now := time.Now()
    ch := make(chan int)

    // 5 expéditeur
    for j := 0; j < 5; j++ {
        wg.Add(1)
        go func() {
            time.Sleep(time.Second * 2)
            i := <-ch
            fmt.Println(i)
            wg.Done()
        }()
    }

    // 6 récepteurs
    for j := 0; j < 6; j++ {
        wg.Add(1)
        go func() {
            time.Sleep(time.Second * 2)
            ch <- 50

            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println(time.Now().Sub(now))
}

Erreur :

50
50
50
50
50
fatal error: all goroutines are asleep - deadlock!

D'après le résultat nous pouvons remarquer que les 5 premières goroutines se sont bien exécutées mais ce n'est pas le cas pour la dernière goroutine. Car cette dernière n'a aucun expéditeur d'où l'erreur deadlock.

buffered channels

Pour éviter le problème d'avant il est possible de rendre nos channels buffered, c'est-à-dire de posséder autant de récepteurs que la taille du buffer de notre channel, par exemple un channel avec une taille de buffer de 30 peut posséder 30 récepteurs.

Pour rendre notre channel buffered, il suffit d'indiquer la longueur de notre buffer comme second argument de notre canal.

ch := make(chan type-de-valeur, taille_du_buffer)
package main

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

var wg = sync.WaitGroup{}

func main() {

    now := time.Now()
    const size int = 10
    ch := make(chan int, size) // channel avec un buffer de taille 10

    // 5 expéditeurs
    for j := 0; j < 5; j++ {
        wg.Add(1)
        go func() {
            time.Sleep(time.Second * 2)
            i := <-ch
            fmt.Println(i)
            wg.Done()
        }()
    }

    // 10 récepteurs
    for j := 0; j < size; j++ {
        wg.Add(1)
        go func() {
            time.Sleep(time.Second * 2)
            ch <- 50
            wg.Done()
        }()
    }

    wg.Wait()

    fmt.Println(time.Now().Sub(now))
}

Résultat :

50
50
50
50
50
2.00064941s

Itération dans un channel

Il est possible d'itérer sur un channel buffered en utilisant le mot clé range. Je vais volontairement provoquer une erreur en itérant sur une channel buffered qui possède moins de récepteurs que la taille du buffer.

package main

    import (
    "fmt"
    "time"
)

func main() {

    ch := make(chan string, 2) // buffer de taille 2

    go func() {
        ch <- "test" // 1 seul récepteur
    }()

    for elem := range ch {
        fmt.Println(elem)
    }

}

Erreur :

test
fatal error: all goroutines are asleep - deadlock!

Alors, déjà on peut remarquer qu'on arrive à lire notre première valeur de notre channel, mais juste après nous avons un deadlock. L'erreur vient du fait qu'on est en train de lire sur un channel avec un buffer de taille 2, sauf que ne nous n'avons rentré qu'une seule valeur dans notre channel d'où l'erreur.

Pour nous prémunir de cette erreur, il faut indiquer à notre compilateur qu'il n'est pas nécessaire de lire la suite du buffer de notre channel, cela est possible avec la fonction close().

package main

import (
	"fmt"
	"time"
)

func main() {

	ch := make(chan string, 2) // buffer de taille 2

	go func() {
		defer close(ch) // on indique à notre compilateur qu'on a finit d'écrire sur le channel
		ch <- "test"
	}()

	for elem := range ch {
		fmt.Println(elem)
	}

}

Résultat :

test

Il existe aussi un autre moyen d'itérer sur un channel, mais avant de vous montrez le code il faut savoir qu'un channel renvoie à la fois sa valeur mais aussi un booléen qui vous indique si la valeur a été envoyée sur le channel.

package main

import (
	"fmt"
)

func main() {
	ch := make(chan string, 2) // buffer de taille 2

	go func() {
		defer close(ch) // on indique à notre compilateur qu'on a finit d'écrire sur le channel
		ch <- "test"
	}()

	for true {
		if elem, ok := <-ch; ok == true { // est ce que le chanel possède encore un récepteur ?
			fmt.Println(elem, ok)
		} else {
			break
		}
	}
}

Résultat :

test

Espace commentaire

Écrire un commentaire

Rejoignez la discussion

Vous devez être connecté pour poster un message.

29 commentaires

ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Si vous avez d'autres soucis avec la synchro, checkez bien vos WaitGroups avant de pointer les channels du doigt.

16/04/2019 à 06:35
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

C'est que tu écris dans un channel que tu as déjà fermé. Il faut centraliser la logique de fermeture dans l'expéditeur principal.

15/04/2019 à 23:34
dbazin
Membre Actif
Avatar de dbazin
dbazin
Membre Actif

Je reçois une erreur bizarre send on closed channel. Comment éviter ça ?

15/04/2019 à 15:43
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Techniquement oui, les deux créent un channel unbuffered.

15/04/2019 à 09:40
josette-bazin
Membre Actif
Avatar de josette-bazin
josette-bazin
Membre Actif

Est-ce que make(chan int) est équivalent à make(chan int, 0) ?

15/04/2019 à 05:31
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Parfait. N'oublie pas d'utiliser defer close(ch) pour être sûr que le channel ferme même en cas de retour précoce.

14/04/2019 à 23:41
allain-adrienne
Membre Actif
Avatar de allain-adrienne
allain-adrienne
Membre Actif

J'ai testé l'exemple du range, ça marche nickel maintenant avec close(ch).

14/04/2019 à 18:26
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Envoie un signal SIGQUIT à ton process (kill -3 PID) et regarde le stacktrace. Go te dira exactement quelle goroutine est bloquée sur quel channel.

14/04/2019 à 12:18
josephine-gautier
Membre Actif
Avatar de josephine-gautier
josephine-gautier
Membre Actif

Comment je peux déboguer facilement quel channel bloque dans mon code ?

14/04/2019 à 05:20
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

La limite est la mémoire disponible sur ta machine. Mais mettre un buffer de 1 million est rarement une bonne idée d'archi.

13/04/2019 à 23:10
rraynaud
Membre Actif
Avatar de rraynaud
rraynaud
Membre Actif

Est-ce qu'il existe une limite de taille pour un channel bufferisé ?

13/04/2019 à 19:10
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Non, le type importe peu pour le blocage. C'est la sémantique de l'opérateur <- qui compte.

13/04/2019 à 13:48
frederic-besson
Membre Actif
Avatar de frederic-besson
frederic-besson
Membre Actif

J'ai un souci avec un channel de pointeurs. Ça change quelque chose au niveau des blocages ?

13/04/2019 à 09:45
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Le sync.WaitGroup gère la fin des goroutines, mais les channels gèrent la communication. Les deux sont complémentaires.

13/04/2019 à 02:16
philippine-leroy
Membre Actif
Avatar de philippine-leroy
philippine-leroy
Membre Actif

Merci pour le tuto. J'ai enfin compris pourquoi mon WaitGroup ne suffisait pas tout seul.

12/04/2019 à 18:18
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Oui, mais il attendra indéfiniment de nouvelles données. C'est le meilleur moyen de créer un deadlock si tu n'as pas de mécanisme d'arrêt.

12/04/2019 à 11:21
suzanne-alexandre
Membre Actif
Avatar de suzanne-alexandre
suzanne-alexandre
Membre Actif

Est-ce qu'on peut utiliser range sur un channel non fermé ?

12/04/2019 à 04:23
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Exactement. Si t'as plus d'expéditeurs que de récepteurs sans buffer, Go te sort un deadlock pour t'éviter de laisser des goroutines orphelines.

11/04/2019 à 20:49
lenoir-etienne
Membre Actif
Avatar de lenoir-etienne
lenoir-etienne
Membre Actif

Mon programme plante avec all goroutines are asleep. C'est bien lié au manque de récepteurs ?

11/04/2019 à 14:11
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Ça dépend de ton besoin de synchro. Le unbuffered garantit une synchro immédiate. Le buffered lisse les pics de charge.

11/04/2019 à 07:24
benoit60
Membre Actif
Avatar de benoit60
benoit60
Membre Actif

Je ne comprends pas bien la différence entre unbuffered et buffered. Lequel est le plus performant pour du traitement intensif ?

11/04/2019 à 00:55
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Utilise la syntaxe val, ok := <-ch. Si ok est false, ton channel est vide et fermé.

10/04/2019 à 17:01
renee47
Membre Actif
Avatar de renee47
renee47
Membre Actif

J'ai une question bête : comment vérifier si un channel est fermé sans provoquer de blocage ?

10/04/2019 à 10:46
ajdaini-hatim
Auteur Rédacteur Secouriste Actif
Avatar de ajdaini-hatim
ajdaini-hatim
Auteur Rédacteur Secouriste Actif

Parce que ton bottleneck est le time.Sleep dans tes goroutines, pas la taille du buffer. Le buffer aide juste à éviter le blocage immédiat.

10/04/2019 à 06:17
regnier-gilbert
Membre Actif
Avatar de regnier-gilbert
regnier-gilbert
Membre Actif

J'ai testé l'exemple avec le buffer à 10. Pourquoi le temps d'exécution reste identique si je mets un buffer de 1 ou 10 ?

10/04/2019 à 01:32

Rejoindre la communauté

Recevoir les derniers articles gratuitement en créant un compte !

S'inscrire