DEV Community

Gwenaël NARDIN
Gwenaël NARDIN

Posted on • Originally published at bittenbypython.com

Programmation concurrente avec des goroutines

Nouvelle année, nouveau challenge ! Ca fait pas mal de temps de je lorgne sur le Go (ou Golang) et ça y est, il est venu le temps de s'y mettre !
Avant cela, j'ai (un peu) mis à jour mon site et rajouter un lien pour ma page LinkedIn : n'hésitez pas à me suivre ou m'ajouter afin de recevoir les prochaines mises à jour 😊.

Le Go (ou Golang) est un langage développé depuis quelques années désormais (plus de 10 ans) par une équipe de Google, et pas des débutants ! Notamment Brian Kernighan, éminent informaticien en C et autre. Ils ont alors développé ce langage pour simplifier l'utilisation du multithreading, principe au coeur du Go.

Je vais vous présenter içi les goroutines, principe de base pour la mise en oeuvre du multithreading en Go.

Définitions

Programmation concurente : composition de plusieurs activités autonomes.

Go supporte deux types de programmation concurrente :

  • CSP (communicating sequential processes - processus séquentiels de communication). Les variables sont passées entre des activités indépendamment mais les variables sont la plus part du temps confinées à une seule de ces activités.
  • shared memory multithreading (multithreading à mémoire partagée)

goroutine : activité exécutée en concurrence

Exemples

Maintenant que nous avons vu le principe de base, place aux exemples !

Basique

On va faire un simple hello world ! mais en utilisant une goroutine. On va écrire une fonction say qui prend une string en argument et que l'on appelera en tant que goroutine (avec le mot-clé go) et qui nous affichera "hello". Voyons ce que ça donne :

package main
import "fmt"


func say(s string) {
    fmt.Println(s)
}

func main() {
    go say("hello")
    fmt.Println("world !")
}
Enter fullscreen mode Exit fullscreen mode

La sortie :

world !
Enter fullscreen mode Exit fullscreen mode

Quoi ?!? Mais il manque un bout !!! 😱😱😱

Explication : la boucle principale main n'attend pas la fin de l'exécution de la goroutine. Pour le moment, on va résoudre ça avec un simple sleep. On modifie le code ainsi :

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    fmt.Println(s)
}

func main() {
    go say("hello")
    fmt.Println("world !")
    time.Sleep(500 * time.Millisecond)
}
Enter fullscreen mode Exit fullscreen mode

On obtient en sortie :

world !
hello
Enter fullscreen mode Exit fullscreen mode

Ah !!!!! Voilà ! On obtient bien ce que l'on souhaite en sortie mais c'est loin d'être optimal...

On peut faire beaucoup mieux !! Place à la suite 😉

Attendre plusieurs goroutines

Lorsque l'on a besoin d'attendre que une ou plusieurs goroutine finissent, on utilise le packet sync et la fonction sync.WaitGroup. Elle est utilisé pour bloquer le main (ou toute autre fonction appelante) jusqu'à ce que toutes les goroutine aient terminé leur travail. Un exemple parlant (adapté de l'article How to wait for all goroutines to finish in Golang) :

package main

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

func worker(wg *sync.WaitGroup, id int) {
    defer wg.Done() //décrémentation (lorsque la goroutine a terminé)

    fmt.Printf("Worker %v: Début\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %v: Terminé\n", id)
}

func main() {
    var wg sync.WaitGroup // déclaration

    for i := 0; i < 5; i++ {
        fmt.Println("Main: Lancement du Worker", i)
        wg.Add(1) // incrémentation d'une unité
        go worker(&wg, i) // passage par adresse
    }

    fmt.Println("Main: En attente des Workers")
    wg.Wait()
    fmt.Println("Main: Terminé")
}
Enter fullscreen mode Exit fullscreen mode

Ce qui donne :

Main: Lancement du Worker 0
Main: Lancement du Worker 1
Main: Lancement du Worker 2
Main: Lancement du Worker 3
Main: Lancement du Worker 4
Worker 0: Début
Worker 4: Début
Worker 2: Début
Main: En attente des Workers
Worker 1: Début
Worker 3: Début
Worker 1: Terminé
Worker 0: Terminé
Worker 4: Terminé
Worker 2: Terminé
Worker 3: Terminé
Main: Terminé
Enter fullscreen mode Exit fullscreen mode

N'est-ce pas formidable ?

L'utilisation de WaitGroup est assez simple :

  • déclation dans le main
  • incrémentation avec .Add(1) avant l'appel de la goroutine
  • appel de la goroutine avec passage par adresse du WaitGroup
  • décrémentation du WaitGroup au sein de la goroutine avec .Done()
  • en fin de bloc, on attend la fin des goroutine avec .Wait()

Notez l'utilisation de defer pour la décrémentation dès le début de la goroutine. Ce mot-clé permet de s'assurer que la fonction qui le suit sera bien appelée à la fin du bloc. On le retrouve pour la lecture des fichiers : on s'assure ainsi que ce dernier sera bien fermé à la fin du traitement.

goroutine anonyme

Lorsque la goroutine ne fait quelques lignes (pas trop, sinon le code devient vite illisible...), il est possible d'utiliser une fonction anonyme. Voyons avec l'exemple précédent ce que cela change :

package main

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

func main() {
    var wg sync.WaitGroup // déclaration

    for i := 0; i < 5; i++ {
        fmt.Println("Main: Lancement du Worker", i)
        wg.Add(1) // incrémentation d'une unité

        go func(id int) { // fonction anonyme
            defer wg.Done() //décrémentation (lorsque la goroutine a terminé)

            fmt.Printf("Worker %v: Début\n", id)
            time.Sleep(time.Second)
            fmt.Printf("Worker %v: Terminé\n", id)
        }(i)
    }

    fmt.Println("Main: En attente des Workers")
    wg.Wait()
    fmt.Println("Main: Terminé")
}
Enter fullscreen mode Exit fullscreen mode

C'est un peu plus concis 😊

Conclusion

Voilà, c'est terminé pour cette petite présentation ! J'ai utilisé également les livres suivant dans mon apprentissage :

A bientôt !

Top comments (0)