Comment by Thaxll

Comment by Thaxll 9 days ago

10 replies

Appending from multiple goroutine to an in un-synchronized slice is "memory safe", it's completely different from c/c++.

It behave exactly like Java or C# which are also memory safe.

tsimionescu 9 days ago

I'm not sure of C#, but Java has stronger memory guarantees than Go, even in the presence of a data race.

In Java, all primitive types (including Object pointers) are atomically modified. And since all Java writes are primitives (Java doesn't have structs), you can never corrupt a data structure at the Java level. Of course, you can still corrupt it at a logical level (break an invariant established in the constructor), but not at the language level.

Go has a guarantee that word-sized reads/writes are atomic, but Go has plenty of larger objects than that. In particular, interface values are "fat pointers" and exceed the word-size on all platforms, so interface writes are not atomic. Which means another thread can observe an interface value having a vtable from one object but data from another, and can then execute a method from one object on data from another object, potentially re-interpreting fields as values of other types.

  • tucnak 9 days ago

    > Which means another thread can observe an interface value having a vtable from one object but data from another, and can then execute a method from one object on data from another object, potentially re-interpreting fields as values of other types.

    If this were the case, then surely someone could construct a program with goroutines, loops and a handful of interface variables—that would predictably fail, right? I wouldn't know how to make one. Could you, or ChatGPT for that matter, make one for demo's sake?

    • kokada 9 days ago

      I am also curious, I keep reading from this thread folks talking that this is possible, but I can't see to find anything searching in Google/DDG.

      There is this document from Golang devs itself[1], that says:

      > Reads of memory locations larger than a single machine word are encouraged but not required to meet the same semantics as word-sized memory locations, observing a single allowed write w. For performance reasons, implementations may instead treat larger operations as a set of individual machine-word-sized operations in an unspecified order. This means that races on multiword data structures can lead to inconsistent values not corresponding to a single write. When the values depend on the consistency of internal (pointer, length) or (pointer, type) pairs, as can be the case for interface values, maps, slices, and strings in most Go implementations, such races can in turn lead to arbitrary memory corruption.

      Fair, this matches what everyone is saying in this thread. But I am still curious to see this in practice.

      [1]: https://go.dev/ref/mem

      Edit: I found this example from Dave Cheney: https://dave.cheney.net/2014/06/27/ice-cream-makers-and-data.... I am curious if I can replicate this in e.g.: Java.

      Edit 2: I can definitely replicate the same bug in Scala, so it is not like Go is unique for the example in that blog post.

      • tsimionescu 9 days ago

        > Edit 2: I can definitely replicate the same bug in Scala, so it is not like Go is unique for the example in that blog post.

        Could you share some details on the program and the execution environment? Per my understanding of the Java memory model, a JVM should not experience this problem. Reads and writes to references (and to all 32 bit values) are explicitly guaranteed to be atomic, even if they are not declared volatile.

        • kokada 7 days ago

              import java.util.concurrent.Executors
              import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}
          
              trait IceCreamMaker {
                def hello(): Unit
              }
          
              class Ben(name: String) extends IceCreamMaker {
                override def hello(): Unit = {
                  println(s"Ben says, 'Hello my name is $name'")
                }
              }
              class Jerry(name: String) extends IceCreamMaker {
                override def hello(): Unit = {
                  println(s"Jerry says, 'Hello my name is $name'")
                }
              }
          
              object Main {
                implicit val context: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2))
          
                def main(args: Array[String]): Unit = {
                  val ben = new Ben("Ben")
                  val jerry = new Ben("jerry")
                  var maker: IceCreamMaker = ben
                  def loop0: Future[Future[Future[Future[Any]]]] = {
                    maker = ben
                    Future { loop1 }
                  }
                  def loop1: Future[Future[Future[Any]]] = {
                    maker = jerry
                    Future { loop0 }
                  }
                  Future { loop0 }
                  while (true) {
                    maker.hello()
                  }
                }
            }
          
          
          Here. I am not saying that JVM shouldn't have a stronger memory model, after thinking for a while I think the issue is the program itself. But feel free to try to understand.
    • tsimionescu 9 days ago

      Sure, here is an example:

      https://go.dev/play/p/_EJ4EvYntr2

      When you run this you will see that occasionally it prints something other than 11 or 100. If it doesn't happen in one run, run it again a few times.

      An equivalent Java program will never print anything else.

      • tucnak 8 days ago

        Thank you, that's really illuminating.

    • [removed] 9 days ago
      [deleted]
kaba0 9 days ago

Not at all. Java or C# can end up in a logical bug from that, but they will never corrupt their runtime. So in java you can just try-catch whatever bad stuff happens there, and go on afterwards.

Go programs can literally segfault from a data race. That's no memory safety.