Cet article est une traduction de la page The Go Memory Model (en anglais sur le site du projet Go).
Ce document – Le modèle de mémoire de Go – spécifie les conditions dans lesquelles on peut garantir que les lectures d'une variable dans une goroutine sont l'observation des écritures dans cette variable par une goroutine différente.
Dans une goroutine en particulier, les lectures et les écritures doivent se comporter selon l'ordre d'exécution spécifié par le programme Go. C'est-à-dire, que les compilateurs et les processeurs ne peuvent changer l'ordre des lectures et des écritures exécutées dans cette goroutine seulement si ce changement de d'ordre d'exécution ne modifie pas le comportement de cette goroutine – comportement défini par les spécifications du langage Go. En raison d'un tel réarrangement, l'ordre d'exécution observé par une goroutine pourrait différer de l'ordre perçu par des autres goroutine. Par exemple, si une goroutine exécute a = 1; b = 2;, les autres goroutines pourraient observer la valeur mise à jour de la variable b avant la valeur mise à jour de la variable a.
Pour définir les conditions de lectures et d'écritures, nous définissons advenir avant, un ordre partiel d'exécution d'opérations sur la mémoire – opérations effectuées par un programme Go. Si l'événement e1 advient avant l'événement e2, alors on dit qu'e2 se produit après e1. En outre, si e1 ne se produit pas avant e2 et n'advient pas après e2, alors on peut dire qu'e1 et e2 se produisent concurremment.
Dans une goroutine en particulier, l'ordre advenir avant est l'ordre exprimé par le programme.
La lecture r d'une variable v n'est autorisée à observer une écriture w dans v seulement si les deux conditions suivantes sont remplies :
Pour garantir que la lecture r d'une variable v observe une écriture particulière w dans v, assurez-vous que w est la seule écriture que r est autorisée à observer. C'est-à-dire que r est garanti d'observer w si les deux conditions suivantes sont remplies :
Ces deux dernières conditions sont plus fortes que les deux premières ; elles exigent qu'il n'y a pas autres écritures advenant en même temps que w ou r.
Dans une goroutine en particulier, il n'y a aucune concurrence (au sein de la goroutine, les actions se déroulent séquentiellement), ainsi les deux définitions sont équivalentes : une lecture r observe la valeur écrite dans v par l'écriture w la plus récente. Lorsque plusieurs goroutines accèdent à une variable partagée v, elles doivent employer des événements de synchronisation afin d'établir les conditions advenir avant qui garantissent que les lectures observent l'écriture souhaitée.
L'initialisation de la variable v avec la valeur zéro (la valeur zéro varie en fonction du type de la variable v) est équivalente à une lecture dans le modèle mémoire que nous sommes en train de décrire dans cet article.
Les lectures et les écritures des valeurs plus grandes qu'un seul mot machine (la taille d'un mot machine dépend du modèle de processeur) se comportent comme plusieurs opérations manipulant un seul mot machine dans un ordre non spécifié (par exemple, l'entier de deux mots de 32 octets 2F221EA1BFB31102 peut être stocké 0211B3BFA11E222F sur certains ordinateurs, alors qu'il peut s'agir que d'un seul mot machine de 64 octets sur d'autres ordinateurs et qu'une autre stratégie de stockage en mémoire peut être adoptée sur d'autres machines).
L'initialisation d'un programme s'exécute dans une seule goroutine, mais cette goroutine peut créer d'autres goroutines, qui fonctionnent concurremment.
Si un paquet p importe le paquet q, l'achèvement des fonctions init de q se produit avant le début de n'importe quelle autre fonction init de p.
Le début de la fonction main.main advient après que toutes les fonctions init soient terminées.
L'instruction go qui lance une nouvelle goroutine se produit avant que l'exécution de cette goroutine ne commence.
Par exemple, dans ce programme :
var a string func f() { print(a) } func hello() { a = "hello, world" go f() }
L'appel à la fonction hello affichera "hello, world" à un certain moment dans le futur (peut-être après le retour de la fonction hello).
Il n'est pas garanti que la sortie d'une goroutine se produise avant les autres événements du programme. Par exemple, dans ce programme :
var a string func hello() { go func() { a = "hello" }() print(a) }
L'affectation de la variable a n'est suivie d'aucun événement de synchronisation, ainsi il n'est pas garanti qu'elle soit observable une autre goroutine. En fait, un compilateur agressif pourrait supprimer l'intégralité de l'instruction go.
Si les effets d'une goroutine doivent être observés par une autre goroutine, il faut employer un mécanisme de synchronisation tel qu'un verrou ou un canal de communication afin d'établir un ordre relatif.
La communication par canal (avec le type chan) est la méthode principale de synchronisation entre les goroutines. Chaque envoi dans un canal particulier est associé à la réception correspondante depuis ce canal, habituellement dans une goroutine différente.
Avec ce programme :Un envoi dans un canal se produit avant que la réception correspondante ne soit reçue de ce canal.
var c = make(chan int, 10) var a string func f() { a = "hello, world" c <- 0 } func main() { go f() <-c print(a) }
Nous avons la garantie que le message "hello, world" sera affiché. L'écriture dans la variable a se produit avant l'envoi dans le canal c – qui se produit avant que la réception correspondante dans le canal c ne s'accomplisse – ce qui advient avant l'affichage (avec print).
La fermeture d'un canal advient avant la réception d'une valeur à zéro parce que ce canal est fermé.
Dans l'exemple précédent, le fait de remplacer c <- 0 par close(c) produit un programme ayant ce comportement.
Une réception depuis un canal sans tampon (buffer) advient avant qu'un envoi dans ce canal ne soit terminé.
Avec ce programme (similaire au précédent, mais avec les instructions d'envoi et de réception permutées et en utilisant un canal sans tampon) :
var c = make(chan int) var a string func f() { a = "hello, world" <-c } func main() { go f() c <- 0 print(a) }
Nous avons également la garantie d'afficher "hello, world". L'écriture dans la variable a se produit avant l'envoi dans le canal c – qui se produit avant que la réception correspondante dans le canal c ne s'accomplisse – ce qui advient avant l'affichage (avec print).
Si le canal avait un tampon (par exemple, s'il était déclaré ainsi : c = make(chan int, 1)) alors il ne serait pas garanti que ce programme afficherait "hello, world" (Il pourrait afficher une chaîne de caractères vide, planter, ou faire autre chose).
Le paquet sync implémente deux types de données de verrouillage, sync.Mutex et sync.RWMutex.
Pour toute variable l de type sync.Mutex ou sync.RWMutex et n précédent m, l'appel n de l.Unlock advient avant que l'appel m de l.Lock n'aboutisse.
Avec ce programme :
var l sync.Mutex var a string func f() { a = "hello, world" l.Unlock() } func main() { l.Lock() go f() l.Lock() print(a) }
Nous avons la garantie que le message "hello, world" sera affiché. Le premier appel à l.Unlock() (dans la fonction f) se produit avant que le deuxième appel à l.Lock() (dans la fonction main) n'aboutisse – ce qui se produit avant l'affichage (avec la fonction print).
Pour tout appel à l.RLock sur une variable l de type sync.RWMutex, il existe n tel que l.RLock advienne (termine son exécution) après l'appel n à l.Unlock et que l'appel correspondant à l.RUnlock advienne avant l'appel n + 1 à l.Lock
Le paquet sync fournit un mécanisme sûr pour l'initialisation, en la présence de multiples goroutines, grâce à l'utilisation du type Once. Plusieurs threads peuvent exécuter once.Do(f) pour une fonction f particulière, mais une seule instance de la fonction f() sera exécutée, et les autres appels seront bloqués jusqu'à ce que f() ait fini son exécution.
Un seul appel de f() par once.Do(f) advient (retourne de son exécution) avant que tout autre appel par once.Do(f) n'advienne.
Dans ce programme :
var a string var once sync.Once func setup() { a = "hello, world" } func doprint() { once.Do(setup) print(a) } func twoprint() { go doprint() go doprint() }
Appeler la fonction twoprint provoque par deux fois l'affichage de "hello, world". Le premier appel à doprint ne lance qu'une fois la fonction setup.
Notez qu'une lecture r peut observer la valeur écrite par une écriture w qui arrive en même temps que r. Même si cela se produit, cela ne signifie pas que lectures advenant après r observeront les écritures qui ont eu lieu avant w.
Dans ce programme:
var a, b int func f() { a = 1 b = 2 } func g() { print(b) print(a) } func main() { go f() g() }
Il peut arriver que la fonction g affiche 2 puis 0.
Ce fait infirme quelques pratiques pourtant communes.
Le verrouillage à double vérification est une tentative pour éviter la surcharge de synchronisation. Par exemple, le programme twoprint pourrait être incorrect s'il était écrit ainsi :
var a string var done bool func setup() { a = "hello, world" done = true } func doprint() { if !done { once.Do(setup) } print(a) } func twoprint() { go doprint() go doprint() }
Mais il n'y a aucune garantie que, dans doprint, observation de l'écriture de la variable done implique d'observer l'écriture de la variable a. Cette version peut (à tort) afficher une chaîne vide au lieu de "hello, world".
Une autre mauvaise pratique consiste à attendre une valeur une valeur en accaparant le processeur, comme dans :
var a string var done bool func setup() { a = "hello, world" done = true } func main() { go setup() for !done { } print(a) }
Comme précédemment, il n'y a aucune garantie que, dans la fonction main, le fait d'observer l'écriture de la variable done implique d'observer l'écriture de la variable a. Donc ce programme pourrait également afficher une chaîne vide. Pire encore, il n'existe aucune garantie que l'écriture de la variable done sera observée dans la fonction main, car il n'y a pas d'événement de synchronisation entre les deux threads. Il n'y a aucune garantie que la boucle principale se termine.
Il existe des variantes subtiles sur ce thème, comme ce programme :
type T struct { msg string } var g *T func setup() { t := new(T) t.msg = "hello, world" g = t } func main() { go setup() for g == nil { } print(g.msg) }
Même si la fonction main observe g != nil et sort de sa boucle, il n'y a aucune garantie que cela observera la valeur initialisée pour g.msg.
Dans tous ces exemples, la solution est la même : utiliser la synchronisation explicite.
Une traduction du blog officiel de golang expliquant le mécanisme de la réflexion en Go. Lire »
Traduction d'un article du blog officiel expliquant comment échanger des données entre deux programmes golang grâce à un format natif Lire »
Exemples d'utilisation du type chan et des go routines stateful et stateless Lire »
Traduction d'une partie des spécifications officielles du langage Go, cet article explique comment développer en Go. Lire »
Préconisations officielles pour la gestion des erreurs dans un programme golang. Cet article complète les explications sur panic, defer et recover Lire »
Soyez le premier à commenter cet article
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.