Aller au contenu

Concurrence

La concurence en Go fait partie du language lui-ême. Ce n’est pas une bibliothèque.

La ysntaxe de Go permet de

  • démarrer en tâche concruurente
  • communiquer avec elle
  • réagire à cette communication

Comme vu dans le Go Tour les primitives de base pour la concurrence sont les goroutines et les cannaux (channels).

Les goroutines sont des fonctions s’exécutant indépendamment dans un espace d’adressage commun

go f()
go g(1, 2)

Les canaux permettent aux goroutines de se synchroniser et d’échanger des messages.

c := make(chan int)
go func() { c <- 3 }()
n := <-c

Ping-Pong

L’exemple ci-dessous illustre lûtilisation de ces primitives :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "fmt"
    "time"
)

type Ball struct{ hits int }

func main() {
    table := make(chan *Ball)
    go player("pong", table)
    go player("ping", table)

    table <- new(Ball) // game on; toss the ball
    time.Sleep(1 * time.Second)
    <-table // game over; grab the ball
}

func player(name string, table chan *Ball) {
    for {
        ball := <-table
        ball.hits++
        fmt.Println(name, ball.hits)
        time.Sleep(100 * time.Millisecond)
        table <- ball
    }
}

En faisant tourner ce programme, nous obtenons le résultat suivant :

ping 1
pong 2
ping 3
pong 4
ping 5
pong 6
ping 7
pong 8
ping 9
pong 10
ping 11

Notez que Go est pourvu d’un détecteur de deadlock et si vous commentez la ligne 15 qui lance la balle sur la table dans le programme précédent, vous obtenez l’erreur suivante au run-time :

fatal error: all goroutines are asleep - deadlock!

Une faiblesse du programme ci-dessus c’est que les deux goroutines qui implémentent les joueurs ne terminent jamais d’elle-même. Elle terminnent bien quand le programme principal termine, mais ce n’est pas très élégant.

Poiur adresses ce problème, nous utilisons la primitive select qui, avec les goroutines et les cannaux est le troisième pilier de la concurrence en Go.

SUpposons que vous souhaitons implémenter une gorooutine qui communique en recevant et en envoyant des messages. Le coeur de la goroutine ressemble à ça:

select {
case xc <- x:
    // sent x on xc
case y := <-yc:
    // received y from yc
}

Le select bloque jusqu’à ce qu’un des case puisse continuer et chaque case est une communication. Dans cet exemple, le premier case essaye d’envoyer la valeur x dans le cannal xc et et deuxième essaye de reçcvoir une valeur du canal yc et la stocke dans la variable y.

Si la fonction ne peut ni écrire, ni lire, elle bloque. Sinon, un seul des case est suivi. Ca veut dire que si y est assigné à la valeur reçue par yc, nous savons que nous n’avons pas envoyé x dans xc.

Lecteur de feed

Pour la suite de ce cours, nous construisons un lecteur de feed (feed reader) qui nous permet de consulter les dernières nouvelles du jour.

Pour implémenter le lecteur, nous avons besoin d’un client RSS.

Le package gofeed disponible sur https://pkg.go.dev/github.com/mmcdole/gofeed nous offre des fonctions permettant de lire un flux RSS.

la fonction ParseURL retourne une instance de type Feed.

type Feed struct {
    Title           string
    Description     string
    ...
    Items           []*Item
    ...
}

L’attribut Items de Feed`` est un tableau de pointeurs vers les articles (Item`)

type Item struct {
    Title           string                   `json:"title,omitempty"`
    Description     string                   `json:"description,omitempty"`
    Content         string                   `json:"content,omitempty"`
    Link            string                   `json:"link,omitempty"`
    Links           []string                 `json:"links,omitempty"`
    ...
}

Vous pouvez tester ce package avec le programme suivant :

package main

import (
    "fmt"

    "github.com/mmcdole/gofeed"
)

func main() {
    fp := gofeed.NewParser()
    feed, _ := fp.ParseURL("https://www.fr.ch/directions-services/71391/rss.xml")
    fmt.Println("---", feed.Title, "---")
    fmt.Println()
    for _, item := range feed.Items {
        fmt.Println(item.Title)
    }
}

Supposons maintenant que nous ne souhaitons pas juste avoir un tableau d’article, mais un canal qui nous délivre les articles. Et même un canal capable de délivrer les articles à plusieurs consommateurs à la fois. Nous implémentons pour cela un modèle d’abonnement :

type Subscription interface {
    Updates() <-chan gofeed.Item // stream of Items
    Close() error                // shuts down the stream
}

func Subscribe(parser gofeed.Feed) Subscription {...} // converts a Feed to a stream

func Merge(subs ...Subscription) Subscription {...} // merges several streams

La fonction Subscribe s’abonne au feed spécifié par le parser et retourne une valeur de type Subscription. Une Subscription est une interface avec une méthode Updates qui retourne un canal qui envoie des Items et une méthode Close qui clos l’abonnement et termine l’envoi d`articles

De plus, Le fonction Merge permet de fusionner plusieurs abonnements en un seul.