Channels and mutable values

197 views
Skip to first unread message

Peter Thatcher

unread,
Nov 11, 2009, 11:47:05 AM11/11/09
to golang-nuts
Let me say again that I love that Rob Pike is reviving CSP with Go.
But I have another question: what do we do about mutable data
structures being sent via channels? For example:

type person struct {
age int; //-1 means "unknown"
}

func grow(chan *people) {
person := <- people;
if person.age < 0 {
*person.age++;
}
}

func hide(chan *people) {
person := <- people;
*person.age = -1;
}

func main() {
people := make(chan *person);
go grow(people);
go hide(people);

bob := new(Person);
bob.age = 30;
people <- bob;
}

Admittedly, this is big contrived, but it easy to run into this in the
real world: two goroutines mutating shared data. In this case, bob
can end up being age 0. Is this possible?

A better example might be a shared cache in a map. Multiple
coroutines would be updating the cache simultaneously, and thus
problems of code interleaving could still be a major problem.

So, what's the right way of avoiding passing mutable structs or maps
in channels? I realize "don't do that" is an easy answer, but it's
not very useful :).

Thank you in advance for any thoughts.

John Cowan

unread,
Nov 11, 2009, 2:00:41 PM11/11/09
to Peter Thatcher, golang-nuts
On Wed, Nov 11, 2009 at 11:47 AM, Peter Thatcher <ptha...@gmail.com> wrote:

> So, what's the right way of avoiding passing mutable structs or maps
> in channels?  I realize "don't do that" is an easy answer, but it's
> not very useful :).

A slightly better answer: passing around mutable objects is really not
CSP at all, it's a hidden and uncontrolled backchannel. So even if
the object is innately mutable, don't mutate it.


--
GMail doesn't have rotating .sigs, but you can see mine at
http://www.ccil.org/~cowan/signatures

Peter Thatcher

unread,
Nov 11, 2009, 2:20:02 PM11/11/09
to John Cowan, golang-nuts
I agree that the solution should be "don't pass mutable objects" or
"don't mutate them". But it appears that all objects in Go are
mutable, and there's no easy way to "change" an object without
mutating it.

It seems like we really need immutable data structures that can be
easily changed (into a new value). This is much like what clojure
does with its "persistent" data structures. That's the kind of thing
you'd want to pass in channels.

Adam Langley

unread,
Nov 11, 2009, 2:21:23 PM11/11/09
to Peter Thatcher, John Cowan, golang-nuts
On Wed, Nov 11, 2009 at 11:20 AM, Peter Thatcher <ptha...@gmail.com> wrote:
> It seems like we really need immutable data structures that can be
> easily changed (into a new value).  This is much like what clojure
> does with its "persistent" data structures.  That's the kind of thing
> you'd want to pass in channels.

You can just pass values rather than pointers to values.


AGL

Ian Lance Taylor

unread,
Nov 11, 2009, 4:33:50 PM11/11/09
to Peter Thatcher, golang-nuts
Peter Thatcher <ptha...@gmail.com> writes:

> But I have another question: what do we do about mutable data
> structures being sent via channels?

The general philosophy is "don't communicate by sharing memory, share
memory by communicating." That is, have a clear owner for all shared
data structures, and pass it around via channels to change ownership.

> So, what's the right way of avoiding passing mutable structs or maps
> in channels? I realize "don't do that" is an easy answer, but it's
> not very useful :).

It's the only answer we have so far, sorry.

Ian

Russ Cox

unread,
Nov 12, 2009, 3:35:38 AM11/12/09
to Peter Thatcher, golang-nuts
On Wed, Nov 11, 2009 at 08:47, Peter Thatcher <ptha...@gmail.com> wrote:
> Let me say again that I love that Rob Pike is reviving CSP with Go.
> But I have another question: what do we do about mutable data
> structures being sent via channels?  For example: [snip]
>
> So, what's the right way of avoiding passing mutable structs or maps
> in channels? I realize "don't do that" is an easy answer, but it's
> not very useful :).

It is more useful than it sounds. If you want to pass a value to many
goroutines, you can send the value (struct, int, etc) on a channel
carrying values, and if you want to pass a pointer, you pass it on
a channel carrying pointers. Both have their place: given

type Point struct {
X, Y int
}

it probably makes sense to use a chan Point instead of a chan *Point
and send values. On the other hand, the RPC code uses a chan *RPCMsg
to good effect: sending the pointer is understood (by the programmer)
to transfer ownership to another goroutine, so while there is a mutable
value being passed around, only one goroutine uses it at a time.

> A better example might be a shared cache in a map. Multiple
> coroutines would be updating the cache simultaneously, and thus
> problems of code interleaving could still be a major problem.

One option is to use a single goroutine to manage the map.
The other option is to use a lock (sync.Mutex). The right option
probably depends on exactly how large the operations are.
If you are managing a couple counters, it is cheaper and simpler
to use a lock than to use a goroutine managing the counters.
If you are managing a complex item like a connection to a remote
RPC server, it probably makes sense to let a single, easily understood
goroutine manage the server and have other goroutines communicate
over channels. The line in the middle is essentially uncharted:
we can't say precisely where it starts to make sense to use channels
instead of mutexes. That line will partly depend on how effiicent the
implementation of channels and scheduling gets. I do know this:
as soon as you would need to utter the words "condition variable",
you've crossed the line and should use a channel. ;-)

> type person struct {
>    age int; //-1 means "unknown"
> }
>
> func grow(chan *people) {
>    person := <- people;
>    if person.age < 0 {
>        *person.age++;
>    }
> }
>
> func hide(chan *people) {
>   person := <- people;
>   *person.age = -1;
> }
>
> func main() {
>    people := make(chan *person);
>    go grow(people);
>    go hide(people);
>
>    bob := new(Person);
>    bob.age = 30;
>    people <- bob;
> }
>
> Admittedly, this is big contrived, but it easy to run into this in the
> real world: two goroutines mutating shared data.  In this case, bob
> can end up being age 0.  Is this possible?

No, but perhaps for a reason you weren't expecting.
Channels do not provide any kind of fan-out: each send
is matched to exactly one receiver. The send people <- bob
can cause bob to be received in grow or hide but not both.
So once all the goroutines have stopped, bob.age is either
still 30 (went to grow) or -1 (went to hide). The fact that
there's no fan-out makes channels well suited to passing
ownership around.

Russ

P. S. There is yet another reason too, completely irrelevant except
that it will bite you if you run this program: when main returns,
the program exits. If a program must wait for the other goroutines
to finish, it needs to do so explicitly. In long-running programs,
a common trick is to put <-make(chan int) at the end of main
and let another goroutine call os.Exit at an appropriate time.
Reply all
Reply to author
Forward
0 new messages