Maison  >  Article  >  développement back-end  >  Modèle de mémoire Vernacular Go Happen-Before

Modèle de mémoire Vernacular Go Happen-Before

Go语言进阶学习
Go语言进阶学习avant
2023-07-24 15:47:581087parcourir

Lorsque plusieurs goroutines accèdent simultanément aux mêmes données, les opérations d'accès simultanées doivent être sérialisées. La sérialisation des lectures et des écritures dans Go peut être garantie via la communication par canal ou d'autres primitives de synchronisation (telles que les verrous mutex, les verrous en lecture-écriture dans le package de synchronisation et les opérations atomiques dans sync/atomic).

Happens Before

Dans une seule goroutine, le comportement de lecture et d'écriture doit être cohérent avec l'ordre d'exécution spécifié par le programme. En d’autres termes, les compilateurs et les processeurs peuvent réorganiser les instructions dans une seule goroutine sans modifier le comportement défini par la spécification du langage.

a := 1
b := 2

En raison de la réorganisation des directives, b := 2 peut précéder a := 1Exécuté. Dans une seule goroutine, l'ajustement de l'ordre d'exécution n'affectera pas le résultat final. Mais des problèmes peuvent survenir dans plusieurs scénarios goroutines. b := 2可能先于a := 1执行。单goroutine中,该执行顺序的调整并不会影响最终结果。但多个goroutine场景下可能就会出现问题。

var a, b int
// goroutine A
go func() {
    a := 5
    b := 1
}()
// goroutine B
go func() {
    for b == 1 {}
    fmt.Println(a)
}()

执行上述代码时,预期goroutine B能够正常输出5,但因为指令重排序,b := 1可能先于a := 5

func sleep() bool {
   time.Sleep(time.Second)
   return true
}

go fmt.Println(sleep())

Lors de l'exécution du code ci-dessus, la goroutine B devrait afficher 5 normalement, mais en raison de la réorganisation des instructions, b := 1 peut précéder a := 5 est exécuté et la goroutine B peut éventuellement générer 0. 🎜
"

Remarque : L'exemple ci-dessus est un exemple incorrect et est uniquement à titre d'illustration.

"

Afin de clarifier les exigences relatives aux opérations de lecture et d'écriture, Go a introduit happens before, qui représente une méthode pour effectuer opérations de mémoire.

Le rôle de ce qui se produit avant

Lorsque plusieurs goroutines accèdent à des variables partagées, elles doivent établir des événements de synchronisation pour garantir que la condition se produit avant afin que les lectures puissent observer les écritures attendues.

Que se passe-t-il avant

Si l'événement e1 se produit avant l'événement e2, alors nous disons que e2 se produit après e1. De même, si e1 n’apparaît ni avant e2 ni après e2, alors on dit que e1 et e2 apparaissent simultanément.

Dans une seule goroutine, l'ordre de ce qui se passe avant est l'ordre d'exécution du programme. Alors, quel est l’ordre des événements avant ? Regardons les conditions ci-dessous.

Si l'opération de lecture r et l'opération d'écriture w pour une variable v satisfont aux deux conditions suivantes, r est autorisé à observer w :

  1. r ne se produit pas avant w.
  2. Aucune autre opération d'écriture ne se produit après w et avant r.

Afin de garantir qu'une opération de lecture r de la variable v peut observer une opération d'écriture spécifique w, il est nécessaire de s'assurer que w est la seule opération d'écriture autorisée à être observée par r. Ensuite, si r et w satisfont tous deux aux conditions suivantes, r peut garantir observer w :

  1. w se produit avant r.
  2. D'autres opérations d'écriture se produisent avant w et après r.

Il n'y a pas de concurrence dans une seule goroutine, ces deux conditions sont équivalentes. Lao Xu a développé cette base et a constaté que ces deux ensembles de conditions sont également équivalents pour un environnement d'exploitation monocœur. Dans le cas de la concurrence, ce dernier ensemble de conditions est plus strict que le premier.

Si vous êtes confus, vous avez raison ! Lao Xu était également confus au début. Ces deux ensembles de conditions étaient les mêmes. Pour cette raison, Lao Xu l'a spécialement comparé à plusieurs reprises avec le texte original pour s'assurer que la compréhension ci-dessus était correcte.

Modèle de mémoire Vernacular Go Happen-Before

Changeons notre façon de penser et procédons à un raisonnement inversé. Si les deux ensembles de conditions sont identiques, il n’est pas nécessaire d’écrire deux fois le texte original. Effectivement, la question n’est pas simple.

Modèle de mémoire Vernacular Go Happen-Before

Avant de poursuivre l'analyse, je voudrais remercier mon professeur de chinois sans vous, je ne pourrais pas découvrir leurs différences.

r n'a pas se produire avant w, la situation possible pour r est que r se produit après w ou en même temps que w, comme le montre la figure ci-dessous (un solide indique qu'il peut se produire en même temps). r没有发生在w之前,则r可能的情况是r发生在w之后或者和w同时发生,如下图(实心表示可同时)。

Modèle de mémoire Vernacular Go Happen-Before

没有其他写操作发生在w之后和r之前

Modèle de mémoire Vernacular Go Happen-Before
Modèle de mémoire Vernacular Go Happen-BeforeAucune autre opération d'écriture ne se produit après w et avant r, puis autre écriture w' peut se produire avant w ou en même temps que w, ou après r ou en même temps que r, comme le montre la figure ci-dessous (un trait plein indique qu'il peut se produire en même temps).

Modèle de mémoire Vernacular Go Happen-BeforeLe deuxième ensemble de conditions est très clair. w se produit avant r et les autres opérations d'écriture ne peuvent se produire qu'avant w ou après r, comme le montre la figure ci-dessous (les espaces vides indiquent qu'elles ne peuvent pas être en même temps). ).

À ce stade, vous devez comprendre pourquoi la deuxième série de conditions est plus stricte que la première. Dans le premier ensemble de conditions, w peut être observé, et dans le deuxième ensemble, w est garanti d'être observé.

Synchronisation dans Go

🎜🎜Voici quelques événements de synchronisation convenus dans Go. Ils peuvent garantir que le programme suit le principe qui se produit avant, de sorte que les goroutines simultanées soient relativement ordonnées. 🎜

Go的初始化

程序初始化运行在单个goroutine中,但是该goroutine可以创建其他并发运行的goroutine。

如果包p导入了包q,则q包init函数执行结束先于p包init函数的执行。main函数的执行发生在所有init函数执行完成之后。

goroutine的创建结束

goroutine的创建先于goroutine的执行。老许觉得这基本就是废话,但事情总是没有那么简单,其隐含之意大概是goroutine的创建是阻塞的。

func sleep() bool {
   time.Sleep(time.Second)
   return true
}

go fmt.Println(sleep())

上述代码会阻塞主goroutine一秒,然后才创建子goroutine。

goroutine的退出是无法预测的。如果用一个goroutine观察另一个goroutine,请使用锁或者Channel来保证相对有序。

Channel的发送和接收

Channel通信是goroutine之间同步的主要方式。

  • Channel的发送动作先于相应的接受动作完成之前。

  • 无缓冲Channel的接受先于该Channel上的发送完成之前。

这两点总结起来分别是开始发送开始接受发送完成接受完成四个动作,其时序关系如下。

开始发送 > 接受完成
开始接受 > 发送完成

注意:开始发送和开始接受并无明确的先后关系

  • Channel的关闭发生在由于通道关闭而返回零值接受之前。

  • 容量为C的Channel第k个接受先于该Channel上的第k+C个发送完成之前。

这里使用极限法应该更加易于理解,如果C为0,k为1则其含义和无缓冲Channel的一致。

Lock

对于任何sync.Mutex或sync.RWMutex变量l以及n < m,第n次l.Unlock()的调用先于第m次l.Lock()的调用返回。

假设n为1,m为2,则第二次调用l.Lock()返回前一定要先调用l.UnLock()。

对于sync.RWMutex的变量l存在这样一个n,使得l.RLock()的调用返回在第n次l.Unlock()之后发生,而与之匹配的l.RUnlock()发生在第n + 1次l.Lock()之前。

不得不说,上面这句话简直不是人能理解的。老许将其翻译成人话:

有写锁时:l.RLock()的调用返回发生在l.Unlock()之后。

有读锁时:l.RUnlock()的调用发生在l.Lock()之前。

注意:调用l.RUnlock()前不调用l.RLock()和调用l.Unlock()前不调用l.Lock()会引起panic。

Once

once.Do(f)中f的返回先于任意其他once.Do的返回。

不正确的同步

错误示范一

var a, b int

func f() {
 a = 1
 b = 2
}

func g() {
 print(b)
 print(a)
}

func main() {
 go f()
 g()
}

这个例子看起来挺简单,但是老许相信大部分人应该会忽略指令重排序引起的异常输出。假如goroutine f指令重排序后,b=2先于a=1发生,此时主goroutine观察到b发生变化而未观察到a变化,因此有可能输出20

老许在本地实验了多次结果都是输出0020这个输出估计只活在理论之中了。

错误示范二

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()
}

这种双重检测本意是为了避免同步的开销,但是依旧有可能打印出空字符串而不是“hello, world”。说实话老许自己都不敢保证以前没有写过这样的代码。现在唯一能想到的场景就是其中一个goroutine doprint执行到done = true(指令重排序导致done=true先于a="hello, world"执行)时,另一个goroutine doprint刚开始执行并观察到done的值为true从而打印空字符串。

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!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer