Le type chan et les goroutines stateless / stateful

Dans cet article nous expliquerons par l'exemple comment développer des fonctions concurrentes avec ou sans état à l'aide du type chan. Les applications web se prêtent bien à ce type de démonstration, car elles sont concurrentes par nature. Il est également plus concret de jouer avec en utilisant plusieurs fenêtres de navigateur.

Un bref rappel sur les canaux

Le type chan est très utilisé dans le langage go. Par exemple, si vous avez lu mon article sur le déclenchement ponctuel ou périodique de fonctions, vous savez que cette fonctionnalité repose sur le type chan – que j'appellerai canal dans la suite de l'article. Le but de l'article n'est pas d'expliquer ce qu'est un canal, mais un léger rappel s'impose.
  • Fonctionnement du type chan

Le canal est un tuyau de communication bloquant entre deux routines go concurrentes. C'est un mécanisme bloquant, car lorsqu'une goroutine lit un canal, son exécution est bloquée jusqu'à ce qu'une autre goroutine écrive dans le canal. Cela peut être utile pour synchroniser ces deux goroutines. Mais attention au fait qu'une goroutine ne doit pas lire dans un canal qui ne serait jamais alimenté, car c'est une situation de blocage.

Cas d'usage

Imaginons une application web dans laquelle une valeur commune à toute l'application peut être mise à jour par les visiteurs. Inversement, les visiteurs peuvent lire cette valeur. On pourrait prendre l'exemple d'un indicateur ou d'un compteur de visites. La bibliothèque standard de go contient de quoi gérer la situation avec le type Mutex. Un Mutex est un mécanisme permettant de verrouiller l'accès à une variable le temps qu'elle soit mise à jour. Pour les types complexes, cela évite un accès en lecture à une variable alors qu'elle ne serait qu'à moitié mise à jour (ce qui – en plus – pourrait provoquer une erreur). Le type Mutex permet donc de partager des données dans un milieu concurrentiel.

Cependant, cette approche va à l'encontre de la philosophie du langage go. Le slogan des concepteurs de golang est :

Ne communiquez pas en partageant la mémoire, mais partagez la mémoire en communicant.

L'équipe Go
Nous allons donc voir deux stratégies de partage de l'information par la communication.

Les goroutines sans état

Avec cette stratégie, n'importe qui peut émettre dans le canal de requête de la valeur commune et il n'y a pas forcément de correspondance entre la demande et la réponse. Par exemple, la demande peut être émise par l'instance n°1 d'un contrôleur (par exemple, la fonction handler exécutée par le visiteur n°1) et la réponse reçue par l'instance n°2 d'un contrôleur. Tandis que l'instance n°2 d'un contrôleur pourrait très bien recevoir la réponse à la demande émise par l'instance n°1 d'un contrôleur. Ce qui n'est pas grave dans le cas d'un compteur de visite, par exemple.
  • Communication vers une routine sans état

Dans le cas où il ne s'agit pas d'une variable unique (comme le compteur de visite), on pourrait très bien lancer plusieurs instances de cette goroutine en parallèle afin d'accélérer le traitement consistant à calculer cette information.

Exemple de code

Si vous êtes désorientés par la manière de coder ce routage HTTP, n'hésitez à jeter un coup d'œil sur mon article à propos des fonctions anonymes. L'idée est de déclarer un canal in pour la mise à jour de la valeur et un canal out afin de demander la valeur. On passe ensuite ces canaux aux deux contrôleurs de notre application :
  • http://localhost:9999/voir Pour visualiser la valeur commune.
  • http://localhost:9999/maj?txtValeur=exemple Pour mettre à jour la valeur commune (ici avec la chaîne de caractères "exemple").
On lance la goroutinevaleurManagerSansEtat à l'aide de l'instruction go en lui passant en paramètre les deux canaux qu'elle doit gérer.
package main

import (
	"net/http"
	"html/template"
)

type Page struct {
	Title		string
	Valeur		string
}

var templates = template.Must(template.ParseFiles("voir.html"))

func main() {
	in := make(chan string)
	out := make(chan string)
	
	go valeurManagerSansEtat(in, out, "Init")
	
	http.HandleFunc("/voir", func(w http.ResponseWriter, r *http.Request) {
		handlerVoirValeur(w, r, request, out)
    })
	http.HandleFunc("/maj", func(w http.ResponseWriter, r *http.Request) {
		handlerMajValeur(w, r, in)
    })
	http.Handle("/",http.FileServer(http.Dir(".")))
	http.ListenAndServe(":9999", nil)
}
La fonction valeurManagerSansEtat s'exécute en parallèle du reste du code. Elle a pour rôle d'attendre l'arrivée d'une valeur dans le canal in. Losqu'elle reçoit une valeur, elle met à jour la valeur commune. Elle scrute aussi le canal out et, peu importe la valeur reçue dans le canal, elle insère la valeur commune dans ce canal. L'instruction for sert ici à réaliser une boucle sans fin. Boucle durant laquelle on vérifie l'état des deux canaux. Note : lorsque l'on définit que le paramètre d'une fonction est de type chan, on peut définir sa direction (il est bidirectionnel par défaut). Indiquer la direction du canal renforce la sécurité de votre application par un typage plus strict (une erreur sera provoquée en cas d'utilisation bidirectionnelle).
func valeurManagerSansEtat(in<- chan string, out chan string, initValue string) {
	valeurCommune := initValue
	for {
		select {
			case valeurNouvelle := <-in:
				valeurCommune = valeurNouvelle
			case <-out:
				out <- valeurCommune	
		}
    }
}
Le contrôleur handlerVoirValeur insère une valeur quelconque dans le canal bidirectionnel out et affiche la valeur lue en retour dans ce même canal. Le template utilisé est très simple (vous trouverez un exemple dans cet article).
func handlerVoirValeur(w http.ResponseWriter, r *http.Request, out chan string) {
	out <- ""
	valeur := <- out
	var p = Page{Title: "Voir la valeur", Valeur: valeur}
	err := templates.ExecuteTemplate(w, "voir.html", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}
Le contrôleur handlerMajValeur récupère le paramètre txtValeur (GET ou POST) qu'il insère dans le canal dédié à la mise à jour de la valeur commune.
func handlerMajValeur(w http.ResponseWriter, r *http.Request, in chan<- string) {
	valeur := r.FormValue("txtValeur")
	in <- valeur
	http.Redirect(w, r, "/voir", http.StatusFound)
}

Les goroutines avec état

L'approche précédente fonctionne très bien, mais il y a certains cas où le principe du pot commun (c.-à-d. le fait de ne pas avoir de garantie ni sur l'ordre des données dépilées dans un canal commun à toutes les goroutines ni sur les émetteurs ou les destinataires) ne conviendrait pas. Formulé autrement, comment gérer les cas où l'on souhaiterait obtenir une réponse à sa question, parce que l'on attend une réponse spécifique. Comme illustré dans ce schéma, le principe est de créer un canal de réponse unique (et temporaire) pour chaque question.
  • Communication vers une routine avec état

Note: le code ci-dessous fait appel à un troisième canal pour gérer la mise à jour de la variable commune. Il n'apparaît pas dans ce schéma afin de l'alléger (et parce que ce n'est pas l'objet de ces derniers paragraphes).

Exemple de code

Si vous êtes désorientés par la manière de coder ce routage HTTP, n'hésitez à jeter un coup d'œil sur mon article à propos des fonctions anonymes. Comme dans l'exemple précédent, l'application répond aux adresses suivantes :
  • http://localhost:9999/voir Pour visualiser la valeur commune.
  • http://localhost:9999/maj?txtValeur=exemple Pour mettre à jour la valeur commune (ici avec la chaîne de caractères "exemple").
La différence avec le code précédent est que le canal gérant les questions / réponses est maintenant de type ValeurQuery. Comme on peut le lire dans le code, cette structure contient un champ de type chan qui servira pour retourner une réponse à l'émetteur qui a demandé la valeur commune.
package main

import (
	"net/http"
	"html/template"
)

type Page struct {
	Title		string
	Valeur		string
}

type ValeurQuery struct {
    canalRetour chan string
}

var templates = template.Must(template.ParseFiles("voir.html"))

func main() {
	maj := make(chan string)
	voir := make(chan ValeurQuery)
	
	go valeurManagerAvecEtat(maj, voir, "Init")
	
	http.HandleFunc("/voir", func(w http.ResponseWriter, r *http.Request) {
		handlerVoirValeur(w, r, voir)
    })
	http.HandleFunc("/maj", func(w http.ResponseWriter, r *http.Request) {
		handlerMajValeur(w, r, maj)
    })
	http.Handle("/",http.FileServer(http.Dir(".")))
	http.ListenAndServe(":9999", nil)
}
Comme expliqué précédemment, on applique un typage strict indiquant que le canal est unidirectionnel (c'est le cas pour les deux paramètres cette fois-ci). Le fonctionnement est le même que dans le code précédent sauf que pour retourner la valeur commune, on utilise le canal temporaire (demande.canalRetour) qui a été créé par l'appelant (le contrôleur handlerVoirValeur).
func valeurManagerAvecEtat(maj<- chan string, voir<- chan ValeurQuery, initValue string) {
	valeurCommune := initValue
	for {
		select {
			case valeurNouvelle := <-maj:
				valeurCommune = valeurNouvelle
			case demande := <-voir:
				demande.canalRetour <- valeurCommune
		}
    }
}
Le contrôleur handlerVoirValeur commence par construire un canal temporaire (dans lequel on lira la réponse de la goroutinevaleurManagerAvecEtat). On initialise la structure ValeurQuery avec ce canal et c'est une variable de type ValeurQuery qui est envoyée à la goroutine via le canal de demande. Contrairement au cas précédent, on peut fermer le canal de réponse après l'avoir reçue, car nous avons construit un canal spécifique à la demande qui ne sera plus utilisé.
func handlerVoirValeur(w http.ResponseWriter, r *http.Request, voir chan<- ValeurQuery) {
	reponse := make(chan string)
    voir <- ValeurQuery{reponse}
	valeur := <-reponse
	close(reponse)
	var p = Page{Title: "Voir la valeur", Valeur: valeur}
	err := templates.ExecuteTemplate(w, "voir.html", p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}
Le contrôleur handlerMajValeur est indentique au cas précédent (Je l'ai reproduit ici, mais ce n'est pas l'objet de ces paragraphes).
func handlerMajValeur(w http.ResponseWriter, r *http.Request, maj chan<- string) {
	valeur := r.FormValue("txtValeur")
	maj <- valeur
	http.Redirect(w, r, "/voir", http.StatusFound)
}

Étiquettes :   golang 
Portrait de Benjamin BALET
Benjamin BALET
Consultant APM

Retrouvez mes cooordonées

Benjamin BALET sur viadeo






Vous aimerez aussi

Les lois de la réflexion

Une traduction du blog officiel de golang expliquant le mécanisme de la réflexion en Go.   Lire »

Gobs le format natif d'échange de données en Go

Traduction d'un article du blog officiel expliquant comment échanger des données entre deux programmes golang grâce à un format natif   Lire »

Le modèle mémoire du runtime et du langage go

Traduction d'une partie des spécifications officielles du langage go, cet article explique comment go gère la mémoire.   Lire »

Comment gérer efficacement les erreurs en golang ?

Préconisations officielles pour la gestion des erreurs dans un programme golang. Cet article complète les explications sur panic, defer et recover   Lire »

Gérer les informations de session avec Gorilla

La bibliothèque standard de go ne gère pas les variables de session d'une application. Il existe une solution avec le toolkit Gorilla   Lire »

Commentaires

Soyez le premier à commenter cet article

Publier un commentaires

Tous les commentaires sont soumis à modération. Les balises HTML sont pour la plupart autorisées. Les liens contextuels et intéressants sont en follow.

(requis)
(requis)
(requis, mais non publié)
(recommandé si vous souhaitez être publié)