Entendiendo el modelo de memoria de Go

Entendiendo el modelo de memoria de Go

Un truco aparentemente inteligente para evitar el bloqueo en código C concurrente es el siguiente: tengo una variable global ptr que apunta a un mystruct y quiero actualizar esa estructura. Así que asignaré un nuevo mystruct, completaré los datos en y solo entonces haré que el cambio sea visible para el mundo señalando ptr al nuevo objeto mystruct.

Esto es incorrecto ya que depende del orden de las escrituras y no hay garantía de que la escritura en ptr sea visible para otros subprocesos después de que todas las tiendas en el nuevo mystruct hayan tomado lugar. Por lo tanto, el nuevo objeto mystruct se puede devolver parcialmente inicializado.

Mi pregunta es: ¿esto también puede suceder en Go? Creo que sí, pero tengo que decir que El modelo de memoria Go me resulta un poco incomprensible.

Escribí un poco de código Go para probarlo, pero en mi máquina, el mal comportamiento no se manifiesta:

package main

import (
    "fmt"
    "time"
)

type mystruct struct {
    a int
    b int
}

var (
    ptr *mystruct
    counter int
)

func writer() {
    for {
        counter += 1
        s := mystruct{a: counter, b: counter}
        ptr = &s
    }
}

func reader() {
    time.Sleep(time.Millisecond)
    for {
        if ptr.a != ptr.b {
            fmt.Println("Oh no, I'm so buggy!")
        }
    }
}

func main() {
    go writer()
    go reader()
    select {}
}

Esto, por supuesto, no prueba nada.

¿Puede proporcionarnos una comparación muy breve de las garantías de memoria proporcionadas por las goroutines de Go con (casi ninguna garantía) proporcionadas por un subproceso POSIX en C?

Mostrar la mejor respuesta

No puedo responder a la q. tal como se planteó, enumere todas las garantías, sin volver a escribir el documento del modelo de mem y tal vez hacer algo mal. Por lo que vale, la solución respaldada oficialmente para publicar una estructura como esa es atomic.Value: golang.org/pkg/sync/atomic/#Value

"Un truco aparentemente inteligente..." - eso no es un truco, sino un lugar común. Pero en realidad no es tan simple y puede causar una variedad de malos comportamientos.

Sin embargo, el código definitivamente es atrevido. No hay nada allí que pueda indicarle al compilador que necesita ordenar los accesos a la memoria de una manera particular, por lo que podría mantener s.a y sb en los registros después de asignarlos a ptr si así lo desea. Sucede que ejecuta esas instrucciones en el orden correcto, y su arco de CPU hizo visibles sus efectos en el orden correcto, pero dado que el código no solicitó ninguna sincronización, eso es solo suerte.

@Olaf No estoy seguro de lo que estás tratando de decir. ¿Podría por favor elaborar un poco? Si por "lugar común" te refieres a un error común, entonces estoy de acuerdo. Por "truco aparentemente inteligente" quise decir que las personas realmente piensan que son geniales cuando se les ocurre algo así. (A mí también me pasó). Especialmente, ¿a qué te refieres con "En realidad no es tan simple"? ¡Su comentario me hace sospechar que tiene una idea y sería genial si lo compartiera!

Es una práctica común, pero requiere ciertas medidas (principalmente usando atómicos) para evitar, p. UB. Ah, y por favor, absténgase de enviar spam a las etiquetas. El solo hecho de mencionar C no justifica agregar la etiqueta.

@twotwotwo "Por supuesto, es un código incorrecto en la Q, pero ayudar a las personas a corregir errores es parte de lo que hacemos en SO". Sí, especialmente cuando la pregunta no es sobre un código en particular, no pido a nadie que corrija mi código y lo agregué como un certificado de no ser tan perezoso y tratando de probar algo yo mismo antes de molestar a otras personas. :)

@Olaf Ad spam, supuse que la persona que respondiera mi pregunta tendría una visión profunda de C para poder comparar su modelo de memoria con el de Go. ¿Está mal usar esa etiqueta en ese caso? Por otra parte, todavía no te entiendo bien (lo siento). "Es una práctica común, pero requiere ciertas medidas (principalmente usando atómicos) para evitar, por ejemplo, UB". Hacer eso en C obviamente está mal y eso es lo que digo en la pregunta. La pregunta era si Go es válido y, en términos más generales, pregunté por las principales diferencias entre Go y C, ya que vengo del mundo C.

C no tiene un modelo de memoria específico. Entonces, no. La implementación de las bibliotecas estándar no se especifica en el estándar. Ni nada más. Una implementación podría usar fácilmente las claves de la base de datos como valores de puntero. Y usar dos (o más) objetos alternos entre dos tareas simultáneas (en el sentido más amplio) no está mal per se, solo tiene que implementarse correctamente. No tengo idea de cómo explicar "práctica/lugar común" más allá de lo que encuentras en un diccionario. Si intenta comparar ciertas características de dos idiomas, es más útil aprender ambos. Hay buenos libros de texto sobre C.

@Olaf ¿Podemos charlar en la sala C, por favor?

El modelo de memoria Go

Versión del 31 de mayo de 2014

Consejos

Si debe leer el resto de este documento para comprender el comportamiento de su programa, está siendo demasiado inteligente.

No seas inteligente.


Presentamos el detector de carreras Go


Yo [David] escribí un poco de código Go para probarlo.

Tu programa Go tiene carreras de datos. Los resultados no están definidos.

$ go run -race david.go
==================
WARNING: DATA RACE
Read at 0x000000596cc0 by goroutine 7:
  main.reader()
      /home/peter/gopath/src/david.go:29 +0x4b

Previous write at 0x000000596cc0 by goroutine 6:
  main.writer()
      /home/peter/gopath/src/david.go:22 +0xf8

Goroutine 7 (running) created at:
  main.main()
      /home/peter/gopath/src/david.go:37 +0x5a

Goroutine 6 (running) created at:
  main.main()
      /home/peter/gopath/src/david.go:36 +0x42
==================
==================
WARNING: DATA RACE
Read at 0x00c0000cc270 by goroutine 7:
  main.reader()
      /home/peter/gopath/src/david.go:29 +0x5b

Previous write at 0x00c0000cc270 by goroutine 6:
  main.writer()
      /home/peter/gopath/src/david.go:21 +0xd2

Goroutine 7 (running) created at:
  main.main()
      /home/peter/gopath/src/david.go:37 +0x5a

Goroutine 6 (running) created at:
  main.main()
      /home/peter/gopath/src/david.go:36 +0x42
==================
==================
WARNING: DATA RACE
Read at 0x00c0000cda38 by goroutine 7:
  main.reader()
      /home/peter/gopath/src/david.go:29 +0x7f

Previous write at 0x00c0000cda38 by goroutine 6:
  main.writer()
      /home/peter/gopath/src/david.go:21 +0xd2

Goroutine 7 (running) created at:
  main.main()
      /home/peter/gopath/src/david.go:37 +0x5a

Goroutine 6 (running) created at:
  main.main()
      /home/peter/gopath/src/david.go:36 +0x42
==================
<<SNIP>>

Tu programa Go: david.go:

package main

import (
    "fmt"
    "time"
)

type mystruct struct {
    a int
    b int
}

var (
    ptr     *mystruct
    counter int
)

func writer() {
    for {
        counter += 1
        s := mystruct{a: counter, b: counter}
        ptr = &s
    }
}

func reader() {
    time.Sleep(time.Millisecond)
    for {
        if ptr.a != ptr.b {
            fmt.Println("Oh no, I'm so buggy!")
        }
    }
}

func main() {
    go writer()
    go reader()
    select {}
}

Patio de juegos: https://play.golang.org/p/XKywmzrRRRw

¿Es posible que no supiera sobre go run -race? ¡Muchas gracias!

Considere atomic.Value si desea "publicar" estructuras como esa para que las lea cualquier otra goroutine: golang.org/pkg/sync/atomic/#Value -- lamentablemente, los documentos de memmodel en realidad no lo discuten, pero los ejemplos y cómo se usa en stdlib indican que ese es un uso aceptado.