Maison > Article > développement back-end > Introduction au modèle de mémoire du langage Go
Le modèle de mémoire de Go détaille les conditions dans lesquelles "une opération de lecture sur une variable dans une goroutine peut détecter une opération d'écriture sur la variable dans une autre goroutine".
Se produit avant
Pour une goroutine, les performances des opérations de lecture et d'écriture des variables doivent être conformes aux attentes du code écrit. En d'autres termes, sans modifier les performances du programme, le compilateur et le processeur peuvent modifier l'ordre des opérations des variables afin d'optimiser le code, c'est-à-dire : les instructions sont réorganisées dans le désordre.
Cependant, lorsque deux goroutines différentes opèrent sur la même variable, différentes goroutines peuvent avoir une compréhension incohérente de la séquence d'opérations variables en raison du réarrangement des instructions. Par exemple, si une goroutine exécute a = 1 ; b = 2 ;, une autre goroutine peut remarquer que la variable b est modifiée avant la variable a.
Afin de résoudre ce problème d'ambiguïté, le langage Go introduit le concept d'arrive avant, qui est utilisé pour décrire l'ordre des opérations de mémoire. Si l’événement e1 se produit avant l’événement e2, nous disons que l’événement e2 se produit après e1.
Si l'événement e1 ne se produit pas avant l'événement e2, et ne se produit pas après e2, on dit que les événements e1 et e2 se produisent en même temps.
Pour une seule goroutine, l'ordre des événements précédents est cohérent avec l'ordre du code.
Si les conditions suivantes sont remplies, un « événement de lecture r » sur la variable v peut percevoir un autre « événement d'écriture w » sur la variable v :
1 « Événement d'écriture w » » se produit avant. "lire l'événement r".
2. Il n'y a pas d'événement d'écriture w dans la variable v qui satisfait à la fois après w et avant r.
Afin de garantir que l'événement de lecture r peut détecter l'événement d'écriture dans la variable v, nous devons d'abord nous assurer que w est le seul événement d'écriture dans la variable v. Dans le même temps, les conditions suivantes doivent être remplies :
1. "Écrire l'événement w" se produit avant "lire l'événement r".
2. Les autres accès à la variable v doivent avoir lieu avant "l'événement d'écriture w" ou après "l'événement de lecture r".
La deuxième série de conditions est plus stricte que la première. Parce que cela nécessite qu'il ne puisse y avoir aucune autre opération de lecture dans le programme où w et r soient exécutés en parallèle.
Pour que les deux ensembles de conditions soient équivalents dans une seule goroutine, les événements de lecture garantissent que les événements d'écriture dans les variables sont détectés. Cependant, pour la variable v partagée entre deux goroutines, nous devons garantir la condition d'occurrence avant via des événements de synchronisation (c'est une condition nécessaire pour que les événements de lecture soient conscients des événements d'écriture).
L'initialisation automatique de la variable v à zéro appartient également à ce modèle d'opération de mémoire.
Lisez et écrivez des données qui dépassent la longueur d'un mot machine, et l'ordre n'est pas garanti.
Synchronisation
Initialisation
L'initialisation du programme se fait dans un outil goroutine séparé. Les goroutines créées lors de l'initialisation seront démarrées une fois que la première goroutine utilisée pour initialiser aura terminé son exécution.
Si le package p importe le package q, la fonction d'initialisation init du package q sera exécutée avant l'initialisation du package p.
La fonction d'entrée main.main du programme est démarrée une fois que toutes les fonctions d'initialisation sont exécutées.
Les goroutines nouvellement créées dans n'importe quelle fonction d'initialisation seront exécutées une fois que toutes les fonctions d'initialisation seront terminées.
Création de Goroutine
L'instruction go utilisée pour démarrer les exécutions de goroutine avant la goroutine.
Par exemple, le programme suivant :
var a string; func f() { print(a); } func hello() { a = "hello, world"; go f(); }
L'appel de la fonction hello affichera "hello, world" à un certain moment (éventuellement après le retour de la fonction hello).
Communication par canal Communication par pipeline
La communication par pipeline est la principale méthode de synchronisation entre deux goroutines. L'usage courant est que différentes goroutines effectuent des opérations de lecture et d'écriture sur le même canal, une goroutine écrit dans le canal et l'autre goroutine lit les données du canal.
L'opération d'envoi sur le canal se produit avant la fin de la réception du canal.
Par exemple, ce programme :
var c = make(chan int, 10) var a string func f() { a = "hello, world"; c <- 0; } func main() { go f(); <-c; print(a); }
peut garantir que "hello, world" sera affiché. Parce que l'affectation de a se produit avant l'envoi des données au canal c, et l'opération d'envoi du canal se produit avant que la réception du canal ne soit terminée. Par conséquent, lors de l’impression, un a été attribué.
Recevez des données d'un canal sans tampon avant d'envoyer des données au canal.
Ce qui suit est un exemple de programme :
var c = make(chan int) var a string func f() { a = "hello, world"; <-c; } func main() { go f(); c <- 0; print(a); }
peut également assurer la sortie de "hello, world". Parce que l'affectation de a se produit avant la réception des données du canal, et l'opération de réception des données du canal se produit avant que l'envoi vers le canal sans tampon ne soit terminé. Par conséquent, lors de l’impression, un a été attribué.
Si vous utilisez un tube tamponné (tel que c = make(chan int, 1)), il n'y a aucune garantie que le résultat "hello, world" sera affiché (il peut s'agir d'une chaîne vide, mais ce ne sera certainement pas une chaîne inconnue), ou provoquera le crash du programme).
Verrouillage
La synchronisation des packages implémente deux types de verrous : sync.Mutex et sync.RWMutex.
对于任意 sync.Mutex 或 sync.RWMutex 变量l。 如果 n < m ,那么第n次 l.Unlock() 调用在第 m次 l.Lock()调用返回前发生。
例如程序:
var l sync.Mutex var a string func f() { a = "hello, world"; l.Unlock(); } func main() { l.Lock(); go f(); l.Lock(); print(a); }
可以确保输出“hello, world”结果。因为,第一次 l.Unlock() 调用(在f函数中)在第二次 l.Lock() 调用(在main 函数中)返回之前发生,也就是在 print 函数调用之前发生。
For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after the n'th call to l.Unlock and the matching l.RUnlock happens before the n+1'th call to l.Lock.
Once
包once提供了一个在多个goroutines中进行初始化的方法。多个goroutines可以 通过 once.Do(f) 方式调用f函数。但是,f函数 只会被执行一次,其他的调用将被阻塞直到唯一执行的f()返回。once.Do(f) 中唯一执行的f()发生在所有的 once.Do(f) 返回之前。
有代码:
var a string func setup() { a = "hello, world"; } func doprint() { once.Do(setup); print(a); } func twoprint() { go doprint(); go doprint(); }
调用twoprint会输出“hello, world”两次。第一次twoprint 函数会运行setup唯一一次。
错误的同步方式
注意:变量读操作虽然可以侦测到变量的写操作,但是并不能保证对变量的读操作就一定发生在写操作之后。
例如:
var a, b int func f() { a = 1; b = 2; } func g() { print(b); print(a); } func main() { go f(); g(); }
函数g可能输出2,也可能输出0。
这种情形使得我们必须回避一些看似合理的用法。
这里用Double-checked locking的方法来代替同步。在例子中,twoprint函数可能得到错误的值:
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(); }
在doprint函数中,写done暗示已经给a赋值了,但是没有办法给出保证这一点,所以函数可能输出空的值。
另一个错误陷阱是忙等待:
var a string var done bool func setup() { a = "hello, world"; done = true; } func main() { go setup(); for !done { } print(a); }
我们没有办法保证在main中看到了done值被修改的同时也 能看到a被修改,因此程序可能输出空字符串。更坏的结果是,main 函数可能永远不知道done被修改,因为在两个线程之间没有同步操作,这样main 函数永远不能返回。
下面的用法本质上也是同样的问题.
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); }
即使main观察到了 g != nil 条件并且退出了循环,但是任何然 不能保证它看到了g.msg的初始化之后的结果。
更多go语言知识请关注PHP中文网go语言教程栏目。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!