Pointers, locks, compare and swap and Go's memory model

1,680 views
Skip to first unread message

John

unread,
Jun 24, 2013, 12:10:15 PM6/24/13
to golan...@googlegroups.com
So I've got a little program that is watching a file for updates via a goroutine.

When that goroutine detects a change, it reads in the data and stores it in a an attribute of a struct, then notifies all the listeners that the file has change.

Kinda like:

func (b *blah) update() {
  // Watch file and update our data when it changes.
  f := Watcher("some file")
  t := f.Content()
  b.content := &t

  // Update all listeners who want to know the file changed.  Only wait for waitTime to prevent blocking.
  for _, c := range b.listeners {
    select {
    case c <- true:
    case time.After(waitTime):
      break
  }
}

My listeners before accessing the content make a pointer copy:

copyPtr := b.content
<do stuff with copyPtr>

I do this to make sure the pointer I'm using doesn't change out while working with the set of data.  

However I'm not using sync.Mutex or other sync primitives to protect the pointer.  I could be sending the data to all listeners through the channel instead of just an update(well, not everywhere, because not all readers listen on a channel.)... But I'm wondering with Go's memory model what the outcome here is going to be?  Could I get away with what I'm doing without sync.Mutex since I have guaranteed synchronous writes with async reads and copy the pointer before reading (the memory model seems to indicate no)?  If I need a lock, could I get away with just atomic.CompareAndSwapPointer around the write and not put sync.Mutex on all my reads.

Testing it a few thousand times seems to work the way I expected it to, but I'm still very doubtful.  I have the feeling that an update will occur at some point and the listener receiving from the update channel will miss the updated data.  

Dmitry Vyukov

unread,
Jun 24, 2013, 12:24:07 PM6/24/13
to John, golang-nuts
This can break.
Update the pointer with atomic.StorePointer (do not use CompareAndSwap
to do a store, use Store to do a store, that's what it is for).
Read the pointer with atomic.LoadPointer.

John Doak

unread,
Jun 24, 2013, 7:19:27 PM6/24/13
to Dmitry Vyukov, golang-nuts
Thanks for the answer.  It leaves me with a few follow-ups:

Can you explain why it breaks in the memory model?

Also, why do StorePoitner and LoadPointer instead of CompareAndSwap?



Henrik Johansson

unread,
Jun 25, 2013, 1:00:50 AM6/25/13
to John Doak, golang-nuts, Dmitry Vyukov

If I understand correctly the two complementary Load/Store methods behave conceptually as a volatile variable in say Java. A memory barrier is inserted in the correct location respectively thus ensuring memory coherence across goroutines (really the underlying threads and cpus).

Reading shared variables like you do in your code is afaik not guaranteed to yield consistent results under the go memory model as per specification.

As for not using swap, well I guess there is no need to swap anything with only one producer?

--
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.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Jesse McNelis

unread,
Jun 25, 2013, 1:15:01 AM6/25/13
to John Doak, Dmitry Vyukov, golang-nuts
On Tue, Jun 25, 2013 at 9:19 AM, John Doak <johns...@gmail.com> wrote:
Thanks for the answer.  It leaves me with a few follow-ups:

Can you explain why it breaks in the memory model?

I'm guessing you're testing on some kind of x86/amd64 where pointer sized assignments are going to be atomic.
But the Go memory model covers additional architectures(ARM etc.) where this isn't guaranteed.
The memory model doesn't guranteee any assignments are atomic. 
Technically you could end up with a half assigned pointer that didn't point to anything.
 
Also, why do StorePoitner and LoadPointer instead of CompareAndSwap?
 
Do you actually care if the pointer has changed since you last saw it?
What are you planning to do if the CompareAndSwap returns false?


 
--
=====================
http://jessta.id.au

Dmitry Vyukov

unread,
Jun 25, 2013, 6:09:09 AM6/25/13
to Henrik Johansson, John Doak, golang-nuts
On Tue, Jun 25, 2013 at 9:00 AM, Henrik Johansson <dahan...@gmail.com> wrote:
> If I understand correctly the two complementary Load/Store methods behave
> conceptually as a volatile variable in say Java.


Yes, Go atomic operations are pretty much the same as Java volatiles.
However, there is no intention (at least for now) to fully define
semantics of data races (as in Java).

Dmitry Vyukov

unread,
Jun 25, 2013, 6:10:31 AM6/25/13
to John Doak, golang-nuts
On Tue, Jun 25, 2013 at 3:19 AM, John Doak <johns...@gmail.com> wrote:
> Thanks for the answer. It leaves me with a few follow-ups:
>
> Can you explain why it breaks in the memory model?

The behavior of programs with data races is undefined.

> Also, why do StorePoitner and LoadPointer instead of CompareAndSwap?

Well, because you need to store a pointer and load a pointer. I would
put the question the other way around -- why CompareAndSwap to store a
value?

John Nagle

unread,
Jun 25, 2013, 2:23:17 PM6/25/13
to golan...@googlegroups.com
On 6/25/2013 3:10 AM, Dmitry Vyukov wrote:
> On Tue, Jun 25, 2013 at 3:19 AM, John Doak <johns...@gmail.com> wrote:
>> Thanks for the answer. It leaves me with a few follow-ups:
>>
>> Can you explain why it breaks in the memory model?
>
> The behavior of programs with data races is undefined.
>
>> Also, why do StorePoitner and LoadPointer instead of CompareAndSwap?
>
> Well, because you need to store a pointer and load a pointer. I would
> put the question the other way around -- why CompareAndSwap to store a
> value?

First, if you're waiting for an event at the file system time
scale, trying to use lockless programming techniques is overkill.
Knocking a few hundred nanoseconds off the locking operation is
pointless for such slow events. Those approaches are usually used
only in very low level programming such as inside the scheduler of a
multiprocessor operating system.

Atomic compare and swap is quite useful, but it's generally used
for explicit synchronization between different CPUs. Read:

http://en.wikipedia.org/wiki/Compare-and-swap

Here's a more detailed discussion of compare-and-swap usage on
x86 Linux kernel implementations:

http://heather.cs.ucdavis.edu/~matloff/50/PLN/lock.pdf

This describes in some detail what's happening at the assembler
and machine level.

If you use a classic compare-and-swap spinlock at the Go
source level, and retry without waiting on a fail, your
program will go into an infinite loop if it ever spinlocks
with GOMAXPROCS=1.

Just use mutexes, until you get to the point that
profiling tells you that your 64-processor supercomputer
is bottlenecking on some simple list update operation.
Or "share memory by communicating, not by sharing memory".

John Nagle

Nathan Youngman

unread,
Jun 26, 2013, 1:46:47 AM6/26/13
to golan...@googlegroups.com

Why not forward the file event onto another channel? (share by communicating)

Here's how I approached file watching:

Correct me if I'm wrong, but I kind've feel like the low-level Mutexes and stuff are appropriate for custom data structures and the like, not necessarily flow control like this.

Nathan.

John

unread,
Jul 2, 2013, 2:41:57 AM7/2/13
to golan...@googlegroups.com
Thank for the explanation and links guys.  This sorted out what I needed to know.  
Sorry I had forgotten the courtesy of a response.
Reply all
Reply to author
Forward
0 new messages