Are receiver copies atomic?

125 views
Skip to first unread message

Ian Davis

unread,
Jun 8, 2021, 6:08:56 AM6/8/21
to golan...@googlegroups.com
This question came to me while reading the recent thread titled "Knowing from documentation whether an interface is holding a pointer or a struct?"

When a method with a non-pointer receiver is called, is the copy made atomically? My intuition says it must be but perhaps someone else can confirm it?

If so, how does it work?

Ian

Jan Mercl

unread,
Jun 8, 2021, 6:21:20 AM6/8/21
to Ian Davis, golang-nuts
On Tue, Jun 8, 2021 at 12:08 PM Ian Davis <m...@iandavis.com> wrote:

> When a method with a non-pointer receiver is called, is the copy made atomically? My intuition says it must be but perhaps someone else can confirm it?

I don't think the specs require that and I don't think well known
implementations try to enforce it. Also, above machine words it gets
complicated.

> If so, how does it work?

The receiver of a method is just the first parameter of an otherwise
ordinary function. I believe it's passed as any other argument,
currently by pushing it to the stack by the caller in case of gc, for
example. gccgo may already use registers, but that I'm only guessing.
(C ABI usually uses registers, but Go with gccgo may differ.)

FTR: AFAICT, from the perspective of the method, its receiver is
passed always by value, regardless if the method is declared on a
pointer receiver or not because a pointer is also a quite normal
value.

jake...@gmail.com

unread,
Jun 8, 2021, 12:17:43 PM6/8/21
to golang-nuts
I'm not 100% sure what you mean by "copy made atomically" means in this context. But if you mean is calling a value receiver method safe for concurrently, then the answer is no.

In fact it can be the source of subtle races. Take for example this simple program (https://play.golang.org/p/Wk5LHxEJ8dQ):

package main
import "fmt"

type Split struct {
    MutValue  uint64
    CostValue uint64
}

func (s Split) GetConstValue() uint64 {
    return s.CostValue
}

var Sum uint64

func main() {
    theOne := Split{7, 3}
    fmt.Print("Start ")

    go func() {
        for {
            theOne.MutValue += 1
        }
    }()

    for {
        Sum += theOne.GetConstValue()
    }
}


If you run this with the race detector on, you will see that there is a race between " theOne.MutValue += 1" and "Sum += theOne.GetConstValue()". At first glance it might seem like this should be ok, since the body of GetConstValue() only reads a non-changing variable, and does not touch MutValue . But in fact calling the function does a copy, and so also reads "MutValue", which is being concurrently modified, hence the race. 

If GetConstValue() is changed to take a pointer receiver, then the race goes away.  

Axel Wagner

unread,
Jun 8, 2021, 1:07:57 PM6/8/21
to Ian Davis, golang-nuts
Not using gc. You can verify that by running this under the race detector (`go run -race main.go`):

package main

type A int

func (A) M() {}

func main() {
    var a A
    go func() { a.M() }()
    a = 1
}


Notably, the function doesn't even have to use the receiver (in fact, the receiver is ignored in this example) for a race to trigger.

I think the only way to get atomic copies would be with something like STM or by guarding ~every access using a mutex transparently. i.e. you need a way to copy arbitrarily large chunks of memory atomically.

On Tue, Jun 8, 2021 at 12:08 PM Ian Davis <m...@iandavis.com> wrote:
--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/cb79c482-42ed-4213-9fc3-381f179e3fa6%40www.fastmail.com.
Reply all
Reply to author
Forward
0 new messages