Go concurrency method

532 views
Skip to first unread message

Alberto Franco

unread,
Aug 19, 2011, 3:40:39 AM8/19/11
to golang-nuts
Hello everybody,
I've done for my "Concurrency Theory" class a simple analysis of Go's
concurrency model. Here is the link to my dropbox public folder:
http://dl.dropbox.com/u/22549284/go.pdf
I'd appreciate some comments and discussion on it. Just to know if I
am totally wrong or some stuff in that document are correct :D.
Enjoy!

Alberto Franco

David Symonds

unread,
Aug 19, 2011, 3:52:43 AM8/19/11
to Alberto Franco, golang-nuts
A few notes:

- The "2008" you mention in the abstract is neither the year of Go's
genesis (2007), or the year of Go's public launch (2009).

- You misspell "to" in the final sentence of the abstract.

- In 2.2, you say "Go borrows some syntax from scripting language such
as Python and Perl, it is not needed to declare the type of a variable
but the compiler infers it from the initialization.". That's highly
inaccurate. Python and Perl don't have static types, so the matter of
type inference is irrelevant for them. In fact, I can't think of
anything in Go's syntax that was taken from Python or Perl.

- 2.3 is also quite inaccurate. panic/recover is *not* an error
handling mechanism. http://golang.org/doc/go_faq.html#exceptions

- In 3.1, you say "Channel communications are synchronous and
one-to-one process." They are *usually* synchronous (buffering can
make them asynchronous), but they are not limited to 1-1. A channel
can often be used to coordinate between many processes (or
"goroutines", more specifically). You explain that in the following
sentences, but why start off with something so misleading?

I stopped at the end of 3.1. I think your code examples are more
complicated than they need to be.


Dave.

Dmitry Vyukov

unread,
Aug 19, 2011, 3:59:59 AM8/19/11
to Alberto Franco, golang-nuts

Yes, Go concurrency model is very different from CSP, because Go
supports plain shared memory and classical shared memory
synchronization primitives. It's fundamental moment. I have no idea
how to describe it formally, but from practical point of view it's
pretty awesome.

Btw, it's possible to implement Barber Shop with only channels in a
quite idiomatic way (IMHO):

seats := make(chan *Customer, 4) // 4 seats

func (self *Customer) EnterShop() bool {
select {
case seats <- self:
default: return false
}
...
}

Dmitry Vyukov

unread,
Aug 19, 2011, 4:04:12 AM8/19/11
to David Symonds, Alberto Franco, golang-nuts
On Fri, Aug 19, 2011 at 11:52 AM, David Symonds <dsym...@golang.org> wrote:
> A few notes:
>
> - The "2008" you mention in the abstract is neither the year of Go's
> genesis (2007), or the year of Go's public launch (2009).
>
> - You misspell "to" in the final sentence of the abstract.
>
> - In 2.2, you say "Go borrows some syntax from scripting language such
> as Python and Perl, it is not needed to declare the type of a variable
> but the compiler infers it from the initialization.". That's highly
> inaccurate. Python and Perl don't have static types, so the matter of
> type inference is irrelevant for them. In fact, I can't think of
> anything in Go's syntax that was taken from Python or Perl.
>
> - 2.3 is also quite inaccurate. panic/recover is *not* an error
> handling mechanism. http://golang.org/doc/go_faq.html#exceptions
>
> - In 3.1, you say "Channel communications are synchronous and
> one-to-one process." They are *usually* synchronous (buffering can
> make them asynchronous)

I would say they are synchronous by *default*. I hope usually they are
not synchronous, because synchronous channels are so inefficient and
prone to deadlocks...

Jim Whitehead II

unread,
Aug 19, 2011, 4:38:43 AM8/19/11
to golang-nuts

The first point is most certainly implementation dependent, and I find
it difficult to believe that the second point is anything other than
your opinion. I do not believe there is any research or literature
that indicates that synchronous message passing is intrinsically more
prone to deadlock than asynchronous message passing. As a concurrency
researcher, my opinion is that this is just a bit of FUD.

- Jim

Alberto Franco

unread,
Aug 19, 2011, 5:03:16 AM8/19/11
to golang-nuts
Thanks for the reply! I will correct as soon as possible the errors
and then re-publish the document. Some more questions:
1. I saw in the documentation from golang.org that 1-to-n
communications are possible but i didn't find any working example of
such communications. I tried to write some examples but i didn't get
the communication right. Do I have to use buffered channels? Have you
a working example?
2. Go type system has been designed from scratch or there is
inspiration from somewhere else in the design of such system? Other
languages or theory?
Thanks again.
Alberto

On Aug 19, 9:52 am, David Symonds <dsymo...@golang.org> wrote:
> A few notes:
>
> - The "2008" you mention in the abstract is neither the year of Go's
> genesis (2007), or the year of Go's public launch (2009).
>
> - You misspell "to" in the final sentence of the abstract.
>
> - In 2.2, you say "Go borrows some syntax from scripting language such
> as Python and Perl, it is not needed to declare the type of a variable
> but the compiler infers it from the initialization.". That's highly
> inaccurate. Python and Perl don't have static types, so the matter of
> type inference is irrelevant for them. In fact, I can't think of
> anything in Go's syntax that was taken from Python or Perl.
>
> - 2.3 is also quite inaccurate. panic/recover is *not* an error
> handling mechanism.http://golang.org/doc/go_faq.html#exceptions

Dmitry Vyukov

unread,
Aug 19, 2011, 5:17:42 AM8/19/11
to Alberto Franco, golang-nuts
On Fri, Aug 19, 2011 at 1:03 PM, Alberto Franco <afra...@gmail.com> wrote:
> Thanks for the reply! I will correct as soon as possible the errors
> and then re-publish the document. Some more questions:
> 1. I saw in the documentation from golang.org that 1-to-n
> communications are possible but i didn't find any working example of
> such communications. I tried to write some examples but i didn't get
> the communication right. Do I have to use buffered channels? Have you
> a working example?

http://code.google.com/p/go/source/browse/src/pkg/runtime/chan_test.go

Dmitry Vyukov

unread,
Aug 19, 2011, 5:52:38 AM8/19/11
to Jim Whitehead II, golang-nuts
On Fri, Aug 19, 2011 at 12:38 PM, Jim Whitehead II <jnwh...@gmail.com> wrote:
>>> A few notes:
>>>
>>> - The "2008" you mention in the abstract is neither the year of Go's
>>> genesis (2007), or the year of Go's public launch (2009).
>>>
>>> - You misspell "to" in the final sentence of the abstract.
>>>
>>> - In 2.2, you say "Go borrows some syntax from scripting language such
>>> as Python and Perl, it is not needed to declare the type of a variable
>>> but the compiler infers it from the initialization.". That's highly
>>> inaccurate. Python and Perl don't have static types, so the matter of
>>> type inference is irrelevant for them. In fact, I can't think of
>>> anything in Go's syntax that was taken from Python or Perl.
>>>
>>> - 2.3 is also quite inaccurate. panic/recover is *not* an error
>>> handling mechanism. http://golang.org/doc/go_faq.html#exceptions
>>>
>>> - In 3.1, you say "Channel communications are synchronous and
>>> one-to-one process." They are *usually* synchronous (buffering can
>>> make them asynchronous)
>>
>> I would say they are synchronous by *default*. I hope usually they are
>> not synchronous, because synchronous channels are so inefficient and
>> prone to deadlocks...
>
> The first point is most certainly implementation dependent,

Why are you so sure? It's a fundamental issue.
I was able to achieve as low overhead as 4 ns per send/recv on
buffered single-producer/single-consumer channels. You can't possibly
achieve that with unbuffered channels. Unbuffered channels inherently
cause context switch for each act of communication with all the
consequences (at least significant amount of additional work + cooling
of data in all caches). Moreover, unbuffered channels limit available
concurrency, namely, in a lot of contexts less goroutines are in
runnable state than it would be otherwise. Something you do not want
in a scalable concurrent system.


> and I find
> it difficult to believe that the second point is anything other than
> your opinion. I do not believe there is any research or literature
> that indicates that synchronous message passing is intrinsically more
> prone to deadlock than asynchronous message passing. As a concurrency
> researcher, my opinion is that this is just a bit of FUD.

Presence of literature does not change things, right?
Search this group for "all goroutines are asleep - deadlock" or try to
extend the following benchmark to more than 2 goroutines:

func BenchmarkChanSync(b *testing.B) {
const CallsPerSched = 1000
procs := 2
N := int32(b.N / CallsPerSched / procs * procs)
c := make(chan bool, procs)
myc := make(chan int)
for p := 0; p < procs; p++ {
go func() {
for {
i := atomic.AddInt32(&N, -1)
if i < 0 {
break
}
for g := 0; g < CallsPerSched; g++ {
if i%2 == 0 {
<-myc
myc <- 0
} else {
myc <- 0
<-myc
}
}
}
c <- true
}()
}
for p := 0; p < procs; p++ {
<-c
}
}


Or try to run the following simple pattern of exchange:

X := make(chan int)
Y := make(chan int)

// goroutine 1
X <- my
his <-Y

// gorotuine 2
Y <- my
his <- X


Regardless of presence of literature, unbuffered channels just do not
support some common idioms.


Taking all that into account and the fact that unbuffered channels do
not make programs deterministic in the general case either, I really
do not understand they are the default in Go, they are rarely the
right choice (especially in the server-side world that Go seem to
target to some degree). I would just remove the default zero capacity
and force a user to specify it explicitly. At least they will be aware
that there is capacity, because as of not I think some user just don't
know/forget there is capacity, and think that either chans are
buffered (there are a lot of queues that are unbounded by default out
there) or just do not think about it at all (but assume that programs
will automagically work, but unbuffered chans are too subtle in some
contexts). Here is a simple example:
c := make(chan int)
c <- some_initial_value_to start_with
...
// start some gorotuines to work with 'c' later

Looks pretty reasonable and nice.
If capacity would be required to specify explicitly, then some users
would not put '0' there initially, or at least the problem would be
more explicit.

Jim Whitehead II

unread,
Aug 19, 2011, 6:15:43 AM8/19/11
to Dmitry Vyukov, golang-nuts

Okay, I understand what you're saying now, sorry for the confusion.

Certainly, but the rendezvous style does can make reasoning about
concurrent programs a bit easier. As many times as people get the 'all
goroutines are asleep', people are able to understand what is
happening. You're absolutely right that for many patterns of
concurrency, the first 'fix' is to make the channels buffered.

> Taking all that into account and the fact that unbuffered channels do
> not make programs deterministic in the general case either, I really
> do not understand they are the default in Go, they are rarely the
> right choice (especially in the server-side world that Go seem to
> target to some degree). I would just remove the default zero capacity
> and force a user to specify it explicitly. At least they will be aware
> that there is capacity, because as of not I think some user just don't
> know/forget there is capacity, and think that either chans are
> buffered (there are a lot of queues that are unbounded by default out
> there) or just do not think about it at all (but assume that programs
> will automagically work, but unbuffered chans are too subtle in some

You could easily choose either as the default, but you make some
compelling arguments. In scala for example, you can't get synchronous
message passing without explicitly implementing it, but in Go it's
just a channel parameter. That might make things more confusing,
indeed.

> contexts). Here is a simple example:
> c := make(chan int)
> c <- some_initial_value_to start_with
> ...
> // start some gorotuines to work with 'c' later
>
> Looks pretty reasonable and nice.
> If capacity would be required to specify explicitly, then some users
> would not put '0' there initially, or at least the problem would be
> more explicit.

Yes, but the default concurrency in Go is based on the CSP model of
concurrency, where *sequential* processes communicate with each other.
When viewed in this model, it makes complete sense why this wouldn't
work.

I think you're precisely right that there are somewhat common idioms
and patterns that are more natural using asynchronous message passing,
but there are certainly cases that can be made clearer by requiring
the rendezvous. I don't know which should be the default =)

Thanks for your response,

- Jim

Dmitry Vyukov

unread,
Aug 19, 2011, 6:53:48 AM8/19/11
to Jim Whitehead II, golang-nuts
On Fri, Aug 19, 2011 at 1:52 PM, Dmitry Vyukov <dvy...@google.com> wrote:
> Taking all that into account and the fact that unbuffered channels do
> not make programs deterministic in the general case either, I really
> do not understand they are the default in Go, they are rarely the
> right choice (especially in the server-side world that Go seem to
> target to some degree). I would just remove the default zero capacity
> and force a user to specify it explicitly. At least they will be aware
> that there is capacity, because as of not I think some user just don't
> know/forget there is capacity, and think that either chans are
> buffered (there are a lot of queues that are unbounded by default out
> there) or just do not think about it at all (but assume that programs
> will automagically work, but unbuffered chans are too subtle in some
> contexts).

This moment worries me for some time. I really would like to hear
thoughts of Go designers on this. You had made that decision, and
there must be some reasons for that. Perhaps I am missing something.
But if not, I consider zero capacity as a very bad default. Since
there is no other possible default value (hummm... maybe 42 will do),
I would leave it without a default at all.

bflm

unread,
Aug 19, 2011, 7:51:59 AM8/19/11
to golan...@googlegroups.com
On Friday, August 19, 2011 12:53:48 PM UTC+2, Dmitry Vyukov wrote:

This moment worries me for some time. I really would like to hear
thoughts of Go designers on this. You had made that decision, and
there must be some reasons for that. Perhaps I am missing something.
But if not, I consider zero capacity as a very bad default. Since
there is no other possible default value (hummm... maybe 42 will do),
I would leave it without a default at all.

I'm not a Go designer, still I would like to share my opinion and experiences. In my code I see quite often both unbuffered and buffered channels used. For me it seems non surprising that - in the case of no buffer size given - it means no buffer, i.e. zero size of it. In other words, I consider the zero capacity as a very good, pretty intuitive, default.

Proper reasoning about deadlocks wrt buffered vs unbuffered channels is enabled simply by writing clean code (with good understanding what one is really doing ;-)

unread,
Aug 19, 2011, 10:28:30 AM8/19/11
to golang-nuts
On Aug 19, 11:52 am, Dmitry Vyukov <dvyu...@google.com> wrote:
> On Fri, Aug 19, 2011 at 12:38 PM, Jim Whitehead II <jnwhi...@gmail.com> wrote:
> [cut]
>
> Why are you so sure? It's a fundamental issue.
> I was able to achieve as low overhead as 4 ns per send/recv on
> buffered single-producer/single-consumer channels. You can't possibly
> achieve that with unbuffered channels.

Send []T through the channel, instead of just single T.

> Unbuffered channels inherently
> cause context switch for each act of communication with all the
> consequences (at least significant amount of additional work + cooling
> of data in all caches). Moreover, unbuffered channels limit available
> concurrency, namely, in a lot of contexts less goroutines are in
> runnable state than it would be otherwise. Something you do not want
> in a scalable concurrent system.

You can create a new goroutine each time you receive a value from a
channel. Goroutines are cheap.

> [cut]
>
> Or try to run the following simple pattern of exchange:
>
> X := make(chan int)
> Y := make(chan int)
>
> // goroutine 1
> X <- my
> his <-Y
>
> // gorotuine 2
> Y <- my
> his <- X
>
> Regardless of presence of literature, unbuffered channels just do not
> support some common idioms.
>
> Taking all that into account and the fact that unbuffered channels do
> not make programs deterministic in the general case either, I really

There is no general relation between [determinism] and [channels being
unbuffered or buffered].

> do not understand they are the default in Go, they are rarely the
> right choice (especially in the server-side world that Go seem to
> target to some degree). I would just remove the default zero capacity
> and force a user to specify it explicitly. At least they will be aware
> that there is capacity, because as of not I think some user just don't
> know/forget there is capacity, and think that either chans are
> buffered (there are a lot of queues that are unbounded by default out
> there) or just do not think about it at all (but assume that programs
> will automagically work, but unbuffered chans are too subtle in some
> contexts).

You are saying this because you are ignoring the problems caused by
having buffered channels as the default. Since you are using trivial
examples to demonstrate that unbuffered channels are evil, I will also
use a trivial example:

// Producer
for { generate(value); ch <- value }

// Consumer
for { value := <-ch; process(value) }

Assuming that "ch" is a buffered channel with a seemingly infinity
capacity, you can use this pattern in a program *only if* you are sure
that the producer is (on average) slower than the consumer. Otherwise,
the program will sooner or later run out of memory. Are you sure all
your producers are running slower than your consumers in your
programs?

The fact is, unbuffered channels result in lower memory consumption.
Smaller working sets are CPU cache friendly. Synchronization of
channel send-receive operations in Go is a way for the consumer to
control the rate with which the producer pushes values through the
channel. Unbuffered channels provide stricter rules when reasoning
about shared memory (http://golang.org/doc/go_mem.html). Why are you
ignoring all of this?

Buffered channels are no magic bullet.

> Here is a simple example:
> c := make(chan int)
> c <- some_initial_value_to start_with
> ...
> // start some gorotuines to work with 'c' later

You are on purpose assuming that programmers do not know what they are
doing (i.e: they are stupid). Why are making this assumption?

Ian Lance Taylor

unread,
Aug 19, 2011, 5:19:31 PM8/19/11
to Dmitry Vyukov, Jim Whitehead II, golang-nuts
Dmitry Vyukov <dvy...@google.com> writes:

> Taking all that into account and the fact that unbuffered channels do
> not make programs deterministic in the general case either, I really
> do not understand they are the default in Go, they are rarely the
> right choice (especially in the server-side world that Go seem to
> target to some degree).

Synchronous channels are the right choice whenever you want to use a
channel to hand off ownership of a shared memory structure. Buffered
channels can be useful when all the associated data is part of the
channel message. Both cases are common.

Ian

Andy Balholm

unread,
Aug 20, 2011, 11:55:48 AM8/20/11
to golan...@googlegroups.com, Jim Whitehead II
On Friday, August 19, 2011 2:52:38 AM UTC-7, Dmitry Vyukov wrote:

I was able to achieve as low overhead as 4 ns per send/recv on
buffered single-producer/single-consumer channels. 

I can only achieve in the 120 ns range with my test code (on an iMac, 3.06 GHz Core 2 Duo and Linux, Phenom 9850). Is your hardware that much better than mine, or am I doing something wrong?

Here's my code:

func BenchmarkChan(b *testing.B) {
        runtime.GOMAXPROCS(1)
        N := b.N
        ch := make(chan int, 100000)

        go func() {
                for i := 0; i < N; i++ {
                        ch <- i
                }
                close(ch)
        }()

        for _ = range ch {
        }
}


If I set GOMAXPROCS to 2, performance drops to 4100 ns/op on Mac OS; on Linux it doesn't drop nearly as much.

P.S. I'm running tip. My previous build of Go took about twice as long per send/receive. It looks like you're making some serious progress on channel performance. Even 120 ns is faster than the memory access time on my old Mac Plus was. But I'd like to know how to achieve 4 ns.

David Roundy

unread,
Aug 20, 2011, 1:44:48 PM8/20/11
to Ian Lance Taylor, Dmitry Vyukov, Jim Whitehead II, golang-nuts

I would also add that synchronous channels does not mean synchronous
communication. I not uncommonly write something like:

go func() {
// when ready, send a page from the source to the sink
req.ch <- (<- cc.pages)
}()

to achieve a nonblocking send to a channel. If I tried to achieve the
same effect via a buffered channel, I'd still have the possibility of
blocking when the buffer is full. So if you *need* asynchronous
communications, buffering isn't the way to get it, unless you know an
upper bound on the possible number of items in the channel.

David

Dmitry Vyukov

unread,
Aug 22, 2011, 9:20:16 AM8/22/11
to ⚛, golang-nuts
On Fri, Aug 19, 2011 at 6:28 PM, ⚛ <0xe2.0x...@gmail.com> wrote:
> On Aug 19, 11:52 am, Dmitry Vyukov <dvyu...@google.com> wrote:
>> On Fri, Aug 19, 2011 at 12:38 PM, Jim Whitehead II <jnwhi...@gmail.com> wrote:
>> [cut]
>>
>> Why are you so sure? It's a fundamental issue.
>> I was able to achieve as low overhead as 4 ns per send/recv on
>> buffered single-producer/single-consumer channels. You can't possibly
>> achieve that with unbuffered channels.
>
> Send []T through the channel, instead of just single T.

It's not always possible or significantly complicated source code.

>> Unbuffered channels inherently
>> cause context switch for each act of communication with all the
>> consequences (at least significant amount of additional work + cooling
>> of data in all caches). Moreover, unbuffered channels limit available
>> concurrency, namely, in a lot of contexts less goroutines are in
>> runnable state than it would be otherwise. Something you do not want
>> in a scalable concurrent system.
>
> You can create a new goroutine each time you receive a value from a
> channel. Goroutines are cheap.

I do not understand what is "cheap". Goroutines are prohibitively
expensive for some otherwise natural usage patterns. For example, when
I do some parallel computations with "goroutines are cheap, you can
create a new goroutine each time you..." my programs are not only
orders of magnitude slower then do not scale as well.


>> [cut]
>>
>> Or try to run the following simple pattern of exchange:
>>
>> X := make(chan int)
>> Y := make(chan int)
>>
>> // goroutine 1
>> X <- my
>> his <-Y
>>
>> // gorotuine 2
>> Y <- my
>> his <- X
>>
>> Regardless of presence of literature, unbuffered channels just do not
>> support some common idioms.
>>
>> Taking all that into account and the fact that unbuffered channels do
>> not make programs deterministic in the general case either, I really
>
> There is no general relation between [determinism] and [channels being
> unbuffered or buffered].

That was just my guess why unbuffered channels are default.


>> do not understand they are the default in Go, they are rarely the
>> right choice (especially in the server-side world that Go seem to
>> target to some degree). I would just remove the default zero capacity
>> and force a user to specify it explicitly. At least they will be aware
>> that there is capacity, because as of not I think some user just don't
>> know/forget there is capacity, and think that either chans are
>> buffered (there are a lot of queues that are unbounded by default out
>> there) or just do not think about it at all (but assume that programs
>> will automagically work, but unbuffered chans are too subtle in some
>> contexts).
>
> You are saying this because you are ignoring the problems caused by
> having buffered channels as the default. Since you are using trivial
> examples to demonstrate that unbuffered channels are evil, I will also
> use a trivial example:
>
> // Producer
> for { generate(value); ch <- value }
>
> // Consumer
> for { value := <-ch; process(value) }
>
> Assuming that "ch" is a buffered channel with a seemingly infinity
> capacity, you can use this pattern in a program *only if* you are sure
> that the producer is (on average) slower than the consumer. Otherwise,
> the program will sooner or later run out of memory.   Are you sure all
> your producers are running slower than your consumers in your
> programs?

Unbounded queues are the root of all evil.
They are not what I was talking about. Moreover there are no unbounded
chans in Go. So with *bounded* buffered chans the program will run
faster will be more scalable and won't OOM.


> The fact is, unbuffered channels result in lower memory consumption.
> Smaller working sets are CPU cache friendly.

Well, buffering is also important for performance. So the answer is
usually some bounded N, not the corner case 0.


> Synchronization of
> channel send-receive operations in Go is a way for the consumer to
> control the rate with which the producer pushes values through the
> channel.

It's equally true for any chan with bounded capacity. There is
principal distinction between 0,1,2,3 and infinity, but not between 0
and 1,2,3.


> Unbuffered channels provide stricter rules when reasoning
> about shared memory (http://golang.org/doc/go_mem.html).

That "reverse synchronization" looks like a sort of anti-pattern. Do
we really want to see people *send* data via global variables by means
of *receive* operations?


> Why are you ignoring all of this?

Well, I perfectly understand that *unbounded* channels are extremely
bad. And I am very glad to see that Go does not feature them.
However, I still consider bounded buffered channels as a better
solution than the corner case of 0 capacity for most situations.


> Buffered channels are no magic bullet.
>
>> Here is a simple example:
>> c := make(chan int)
>> c <- some_initial_value_to start_with
>> ...
>> // start some gorotuines to work with 'c' later
>
> You are on purpose assuming that programmers do not know what they are
> doing (i.e: they are stupid). Why are making this assumption?

I periodically have deadlock problems with sync channels, and I see
that some people on this mailing list have them as well.
But deadlocks aside, buffered channels provide better performance and
scalability.

Dmitry Vyukov

unread,
Aug 22, 2011, 9:24:14 AM8/22/11
to golan...@googlegroups.com, Jim Whitehead II
On Sat, Aug 20, 2011 at 7:55 PM, Andy Balholm <andyb...@gmail.com> wrote:
> On Friday, August 19, 2011 2:52:38 AM UTC-7, Dmitry Vyukov wrote:
>>
>> I was able to achieve as low overhead as 4 ns per send/recv on
>> buffered single-producer/single-consumer channels.
>
> I can only achieve in the 120 ns range with my test code

That was single-producer/single-consumer queue in C. It uses
substantially different algorithm than current Go implementation. Go
implementation can be improved to some degree (I have the CL
somewhere, but it has some problems with selects and more importantly
it complicates things considerably), but since it is
multi-producer/multi-consumer queue it can possibly catch up with that
queue in this decade.

Dmitry Vyukov

unread,
Aug 22, 2011, 9:27:36 AM8/22/11
to Ian Lance Taylor, Jim Whitehead II, golang-nuts

I do not see how it is related. May you elaborate?
In all examples I can imagine buffering and embedness are orthogonal.

Dmitry Vyukov

unread,
Aug 22, 2011, 9:33:41 AM8/22/11
to David Roundy, Ian Lance Taylor, Jim Whitehead II, golang-nuts
On Sat, Aug 20, 2011 at 9:44 PM, David Roundy
<rou...@physics.oregonstate.edu> wrote:
> On Fri, Aug 19, 2011 at 2:19 PM, Ian Lance Taylor <ia...@google.com> wrote:
>> Dmitry Vyukov <dvy...@google.com> writes:
>>
>>> Taking all that into account and the fact that unbuffered channels do
>>> not make programs deterministic in the general case either, I really
>>> do not understand they are the default in Go, they are rarely the
>>> right choice (especially in the server-side world that Go seem to
>>> target to some degree).
>>
>> Synchronous channels are the right choice whenever you want to use a
>> channel to hand off ownership of a shared memory structure.  Buffered
>> channels can be useful when all the associated data is part of the
>> channel message.  Both cases are common.
>
> I would also add that synchronous channels does not mean synchronous
> communication.  I not uncommonly write something like:
>
> go func() {
>  // when ready, send a page from the source to the sink
>  req.ch <- (<- cc.pages)
> }()

Looks like a hack...

> to achieve a nonblocking send to a channel.  If I tried to achieve the
> same effect via a buffered channel, I'd still have the possibility of
> blocking when the buffer is full.  So if you *need* asynchronous
> communications, buffering isn't the way to get it, unless you know an
> upper bound on the possible number of items in the channel.

It's not a solution. You are basically trying to model an unbounded
channel in a very inefficient manner. And unbounded channels are the
root of all evil. While number of pending elements is below some
reasonable capacity, it will work just as a bounded channel. Otherwise
it will crash the system badly.

Andy Balholm

unread,
Aug 22, 2011, 11:51:30 AM8/22/11
to golan...@googlegroups.com, Jim Whitehead II
On Monday, August 22, 2011 6:24:14 AM UTC-7, Dmitry Vyukov wrote:
>> I was able to achieve as low overhead as 4 ns per send/recv on
>> buffered single-producer/single-consumer channels.

That was single-producer/single-consumer queue in C. It uses


substantially different algorithm than current Go implementation. Go
implementation can be improved to some degree (I have the CL
somewhere, but it has some problems with selects and more importantly
it complicates things considerably), but since it is
multi-producer/multi-consumer queue it can possibly catch up with that
queue in this decade.

So channel send/receive operations effectively spend 97% of their time dealing with the possibility that another goroutine might be trying to send or receive on the channel at the same time?

A very significant percentage (maybe even a majority) of uses of channels are actually single-producer/single-consumer. Would it be possible/practical/acceptable to add a way to declare a channel as sp/sc and use a more efficient implementation in those cases? 

Of course, if someone declared a channel as sp/sc and then tried to send on it from 2 goroutines simultaneously, it would cause interesting errors—but probably no worse than updating global variables simultaneously without synchronization, which is already possible in Go.

Andy

Jim Whitehead II

unread,
Aug 22, 2011, 11:59:20 AM8/22/11
to golan...@googlegroups.com

Uniqueness types (or some derivative) would solve that issue.

- Jim

Andy Balholm

unread,
Aug 22, 2011, 12:05:28 PM8/22/11
to golan...@googlegroups.com, Jim Whitehead II
On Monday, August 22, 2011 8:51:30 AM UTC-7, Andy Balholm wrote:
 Would it be possible/practical/acceptable to add a way to declare a channel as sp/sc and use a more efficient implementation in those cases? 

On second thought, it would probably be better to have it as a third argument for make() than as part of the declaration. Then which implementation to use could be a run-time decision. Not only that, but the sp/sc implementation could not be used without specifying a buffer size (the second argument for make).

Andy

Ian Lance Taylor

unread,
Aug 22, 2011, 1:03:14 PM8/22/11
to Dmitry Vyukov, Jim Whitehead II, golang-nuts
Dmitry Vyukov <dvy...@google.com> writes:

You're right, I was entangling a couple of different ideas. What I'm
trying to get at is that synchronous channels give you a synchronization
point, which is useful when manipulating a complex shared data
structure. Most obviously, goroutines can pass around a token
indicating temporary ownership of the structure--the token would
probably be a pointer to the structure. This only works with
synchronous channels.

Ian

Kyle Lemons

unread,
Aug 22, 2011, 1:16:18 PM8/22/11
to Dmitry Vyukov, ⚛, golang-nuts
I periodically have deadlock problems with sync channels, and I see
that some people on this mailing list have them as well.
But deadlocks aside, buffered channels provide better performance and
scalability.

FWIW, the worst and hardest-to-reproduce deadlocks that I have created are those introduced by buffered channels (and incidentally, unbuffering all of the channels involved makes the deadlock reproducible and debuggable).  One of the biggest misconceptions of which I had to disabuse myself was the notion that buffered channels are somehow less deadlock prone than unbuffered ones; all it does is make it block *less often* not never.

~K

Dmitry Vyukov

unread,
Aug 22, 2011, 3:19:03 PM8/22/11
to golan...@googlegroups.com, Jim Whitehead II

I forget to mention that that queue was nonblocking, in the sense that
recv from empty queue/send to full queue just returns false. Go chans
has blocking semantics, it inevitably add more overhead.

I don't think it's a good idea to add single-producer/single-consumer
chans to Go as a first class concept.
However, if/when templates/generics are in Go, it's quite easy to
implement it in a separate package.

Dmitry Vyukov

unread,
Aug 22, 2011, 3:25:38 PM8/22/11
to Ian Lance Taylor, Jim Whitehead II, golang-nuts

Hummm... I still don't get it. I understand the additional
synchronization provided by sync channels, however I don't see how
that "reverse" synchronization makes difference in this case. If a
goroutine sends (hand offs) a token/pointer to an object to a channel,
the only thing that matter is whether the goroutine continues to use
the object or not. The channel can be buffered or not, it's
irrelevant. The goroutine can hand off several objects via a buffered
chan at a time. A classical example is pipeline processing - input
stage reads files from disk and hand offs them to processing state,
then processing state hand off processed data to output stage. It's
better implemented with buffered channels.

Dmitry Vyukov

unread,
Aug 22, 2011, 3:30:10 PM8/22/11
to Kyle Lemons, ⚛, golang-nuts

I see your point. Sometimes buffered channels make deadlocks less
likely to occur, and consequently harder to debug. However, sometimes
buffered channels do eliminate deadlocks. Namely, when capacity is
larger or equal to number of messages sent (by single goroutine or all
goroutines depending on the situation). Sometimes we just need to send
a single message and we know then chan capacity is >1. Sometimes we
know that N messages will be send and create chan with capacity N.

Ian Lance Taylor

unread,
Aug 22, 2011, 3:35:01 PM8/22/11
to Dmitry Vyukov, Jim Whitehead II, golang-nuts
Dmitry Vyukov <dvy...@google.com> writes:

I'm sorry, you're right. I thought that the memory model treated
unbuffered channels differently in this regard, but I looked at it again
and I was mistaken.

Do synchronous channels give any performance advantage?

Ian

Dmitry Vyukov

unread,
Aug 22, 2011, 4:01:12 PM8/22/11
to Ian Lance Taylor, Jim Whitehead II, golang-nuts
On Mon, Aug 22, 2011 at 11:35 PM, Ian Lance Taylor <ia...@google.com> wrote:
>> Hummm...  I still don't get it. I understand the additional
>> synchronization provided by sync channels, however I don't see how
>> that "reverse" synchronization makes difference in this case. If a
>> goroutine sends (hand offs) a token/pointer to an object to a channel,
>> the only thing that matter is whether the goroutine continues to use
>> the object or not. The channel can be buffered or not, it's
>> irrelevant. The goroutine can hand off several objects via a buffered
>> chan at a time. A classical example is pipeline processing - input
>> stage reads files from disk and hand offs them to processing state,
>> then processing state hand off processed data to output stage. It's
>> better implemented with buffered channels.
>
> I'm sorry, you're right.  I thought that the memory model treated
> unbuffered channels differently in this regard, but I looked at it again
> and I was mistaken.
>
> Do synchronous channels give any performance advantage?


No, they don't. On the opposite. They have inherent overhead of
goroutine rescheduling + potentially even more severe overhead of
worker thread parking/unparking if there are no other runnable
goroutines. Moreover, they potentially limit available concurrency in
the system. Think of the pipeline example (file input -> processing ->
file output), one really wants buffered channels between stages.
Buffered channels on the opposite allow for a nice streaming of data
and maximize available concurrency.

I still think it's a good idea to remove default zero capacity from
chan make. Zero default capacity is OK for slices, because it does not
play significant role, I can as if increase it later. But for chans it
plays crucial role.

Paul Borman

unread,
Aug 22, 2011, 4:09:24 PM8/22/11
to Dmitry Vyukov, Ian Lance Taylor, Jim Whitehead II, golang-nuts
On Mon, Aug 22, 2011 at 1:01 PM, Dmitry Vyukov <dvy...@google.com> wrote:
I still think it's a good idea to remove default zero capacity from
chan make. Zero default capacity is OK for slices, because it does not
play significant role, I can as if increase it later. But for chans it
plays crucial role.

So you would make

    c := make(chan int)

illegal and replace it with:

    c := make(chan int, 0)

so people would need to explicitly say if they wanted buffered or unbuffered channels?

bflm

unread,
Aug 22, 2011, 4:13:35 PM8/22/11
to golan...@googlegroups.com
On Monday, August 22, 2011 10:01:12 PM UTC+2, Dmitry Vyukov wrote:
> Do synchronous channels give any performance advantage?


No, they don't. On the opposite. They have inherent overhead of
goroutine rescheduling + potentially even more severe overhead of
worker thread parking/unparking if there are no other runnable
goroutines.

That's true but in the same time it can be unimportant. Imagine a sync channel which only announces/waits for some part/all parts of a job is done.

Moreover, they potentially limit available concurrency in
the system. Think of the pipeline example (file input -> processing ->
file output), one really wants buffered channels between stages.

So simply one would then use buffered channels. No need to force it in any way.

Buffered channels on the opposite allow for a nice streaming of data
and maximize available concurrency.

Yes. They can/mostly serve a different purpose than sync channels. 

I still think it's a good idea to remove default zero capacity from
chan make.

Notice how I much agree with you above. Still I object to this conclusion. No capacity explicitly stated => no capacity is IMO very good. The decision about the buffer size is to be made by the programmer, *not* by the language.

Zero default capacity is OK for slices, because it does not
play significant role, I can as if increase it later. But for chans it
plays crucial role.

Sometimes yes, sometimes no. No one knows that in general and in advance.

Dmitry Vyukov

unread,
Aug 22, 2011, 4:57:12 PM8/22/11
to Paul Borman, Ian Lance Taylor, Jim Whitehead II, golang-nuts

Yes, that was what I meant.
Potentially, make(chan *Request) is replaced with make(chan *request,
100) (manually, not by gofix of course). I remember I saw some
external library in this mailing list that extensively used unbuffered
chans where is really should use buffered chans (unfortunately, I
don't remember library name now).

However, maybe it's a very good idea.
To make it clear. In the first place, I wanted to understand why the
decision was made, and is there something good about sync channels I
am missing. Now the things are more clear to me. Alternatively, now I
would just warn users in tutorials/etc that they must clearly
understand what make(chan int) is; that it does not create "a queue"
that they are used to in other languages/libraries; and that they
should prefer to intelligently choose chan capacity. "Effective Go"
section on channels seems to be quite clear at least on semantics.
Well, the only problem with this route is that developers tend to
ignore documentation :)

Dmitry Vyukov

unread,
Aug 22, 2011, 5:02:20 PM8/22/11
to golan...@googlegroups.com
On Tue, Aug 23, 2011 at 12:13 AM, bflm <befeleme...@gmail.com> wrote:
> On Monday, August 22, 2011 10:01:12 PM UTC+2, Dmitry Vyukov wrote:
>>
>> > Do synchronous channels give any performance advantage?
>>
>> No, they don't. On the opposite. They have inherent overhead of
>> goroutine rescheduling + potentially even more severe overhead of
>> worker thread parking/unparking if there are no other runnable
>> goroutines.
>
> That's true but in the same time it can be unimportant. Imagine a sync
> channel which only announces/waits for some part/all parts of a job is done.

Then a user would explicitly specify zero capacity. If it makes a user
to explicitly think about what type of chan he need, it's good.


>> Moreover, they potentially limit available concurrency in
>> the system. Think of the pipeline example (file input -> processing ->
>> file output), one really wants buffered channels between stages.
>
> So simply one would then use buffered channels. No need to force it in any
> way.

I was not talking about forcing something.


>> Buffered channels on the opposite allow for a nice streaming of data
>> and maximize available concurrency.
>
> Yes. They can/mostly serve a different purpose than sync channels.
>>
>> I still think it's a good idea to remove default zero capacity from
>> chan make.
>
> Notice how I much agree with you above. Still I object to this conclusion.
> No capacity explicitly stated => no capacity is IMO very good. The decision
> about the buffer size is to be made by the programmer, *not* by the
> language.

Indeed. Currently it's sort of made by the language if I do not do it.


>> Zero default capacity is OK for slices, because it does not
>> play significant role, I can as if increase it later. But for chans it
>> plays crucial role.
>
> Sometimes yes, sometimes no. No one knows that in general and in advance.

Indeed. That's why I say that defaults are problematic here.

Kyle Lemons

unread,
Aug 22, 2011, 5:07:33 PM8/22/11
to Dmitry Vyukov, golang-nuts
While, in principle, I don't see any problem with making the size an explicit argument, I do take issue with the premise for doing so.  The thought seems to be that by making programmers specify the size, we're making their programs more optimized, because they choose a size that's applicable for them.  I think that there are enough people using go (and in particular, those who "forget" that channels can have a capacity) who are new enough to the language that synchronous channels make a very sensible default, even if their particular application might be more optimal with a buffer.

Consider the following:

signal := make(chan bool)
for {
  doSomething()
  signal <- true
}

There is a huge difference in the behavior of this loop if signal is synchronous versus asynchronous.  I can control, to a greater or lesser degree, when and how often doSomething is called by only reading when I'm ready for it to get called again when it's synchronous, but I can't do any such thing in the async case.  Newcomers to Go will absolutely be tempted to make their channels asynchronous for the "performance benefit" and will get used to typing "make(chan blah, 10)" or something and then will have no idea what's going on when they try to use a model like the above with a channel they made with an arbitrary buffer.  Sure, they might do this anyway, and sure, if they create the channel at the same time as they write the synchronized loop they will probably notice that it should be unbuffered, but there are plenty of times where they might be trying to repurpose or reuse an existing channel.

I think mandating the second argument is encouraging premature optimization.  Allowing people who need performant channels to use buffering to optimize their code is fine, but trying to get your average programmer to use them more often seems like a recipe for disaster.
~K

Dmitry Vyukov

unread,
Aug 22, 2011, 5:26:41 PM8/22/11
to Kyle Lemons, golang-nuts
On Tue, Aug 23, 2011 at 1:07 AM, Kyle Lemons <kev...@google.com> wrote:
> While, in principle, I don't see any problem with making the size an
> explicit argument, I do take issue with the premise for doing so.

All you are saying makes sense. The problem with my vision of things
is that it's not always in line with a vision of a typical user...
As I said, the first goal of this discussion was for me to understand
the situation (perhaps other people pick up something useful here as
well). Then, I just raised the issue. I do not afraid to do that,
because I do know that Go team consists of extremely reasonable
people, and if they are not sure in the change they will just say "No"
:)

Paul Borman

unread,
Aug 22, 2011, 6:17:44 PM8/22/11
to Dmitry Vyukov, Kyle Lemons, golang-nuts
I think it comes down to, should we expect people to be able to write effective go programs without understanding what they are writing?

Anyone who uses a facility that they do not understand will likely end up using it wrong.  The question, in my mind, does the expression of the concept match the complexity of the concept and does the expression give rise to unexpected results?

A person who uses channels without understanding anything about buffering will likely not be able to write an effective go program that uses channels.  If they do take the time to learn what a channel is and what buffering is before they use it, does the current expression get in their way?  To me the answer is no.

In Go, 0 is the default for everything (a zeroed structure).  So

    c := make(chan int)

is pretty clear that the unspecified capacity is 0.  The only argument I would see would be that there should be no optional parameters.  make always takes 2 arguments for a channel, and 3 for a slice.

I don't see any valid argument for making the change to make programs more correct or higher performance.  In most cases, if you changed my c := make(chan foo) to c := make(chan foo, 100) you would break my program because you have fundamentally changed the semantics of what c is.

Anh Hai Trinh

unread,
Aug 22, 2011, 10:13:56 PM8/22/11
to Dmitry Vyukov, Kyle Lemons, golang-nuts
A bit off topic, but I have a question about async chan: how do you
know how much buffer is optimal? What could it depend on
theoretically? Or must it be arrived after much profiling and
measurement for each individual program / communicating system?

--
@chickamade

Brad Fitzpatrick

unread,
Aug 22, 2011, 11:58:35 PM8/22/11
to Anh Hai Trinh, Dmitry Vyukov, Kyle Lemons, golang-nuts
On Tue, Aug 23, 2011 at 6:13 AM, Anh Hai Trinh <anh.ha...@gmail.com> wrote:
A bit off topic, but I have a question about async chan: how do you
know how much buffer is optimal? What could it depend on
theoretically? Or must it be arrived after much profiling and
measurement for each individual program / communicating system?

This has been bugging me ever since starting to use Go.

I ask and the answer I receive is "oh, just make it a small buffer".  What is small?

I keep hoping that Go will add a make(chan foo, buffered) where you say "bounded, but buffered and sane automatic size for throughput".  Not having that, what I end up doing in all my packages where I want that is:

const buffered = 32 // some arbitrary size
...
   c := make(chan *Foo, buffered)
...

Then it at least reads easier.  If I instead wrote "32" there instead of "buffered", readers would ask "why 32? is that special or important?"

David Leimbach

unread,
Aug 23, 2011, 1:36:47 AM8/23/11
to golang-nuts


On Aug 19, 2:19 pm, Ian Lance Taylor <i...@google.com> wrote:
> Dmitry Vyukov <dvyu...@google.com> writes:
> > Taking all that into account and the fact that unbuffered channels do
> > not make programs deterministic in the general case either, I really
> > do not understand they are the default in Go, they are rarely the
> > right choice (especially in the server-side world that Go seem to
> > target to some degree).
>
> Synchronous channels are the right choice whenever you want to use a
> channel to hand off ownership of a shared memory structure.  Buffered
> channels can be useful when all the associated data is part of the
> channel message.  Both cases are common.
>

For distributed applications, buffered channels are pretty much the
natural feeling way to go in my opinion. It forces one to deal with
the idea, early, that rendezvous isn't always possible (nodes fail,
networks partition), and by writing applications where the actors work
locally in an asynchronous way seems to make it easier to plan for
such failures when making an application distributed later.

Maybe one can get around it with more goroutines and handing off
responsibilities when ones waiting on an RPC reply. The Erlang
programmer in me just doesn't do stuff that way. I suppose as long as
you can time out on receiving a message you can get by either way.

Maybe with closures, one can abstract it enough that it doesn't matter
too much. (run a returned closure for completion and wrap up the
communication). Sounds like a fun experiment.


> Ian

Dmitry Vyukov

unread,
Aug 23, 2011, 4:05:20 AM8/23/11
to Anh Hai Trinh, Kyle Lemons, golang-nuts
On Tue, Aug 23, 2011 at 6:13 AM, Anh Hai Trinh <anh.ha...@gmail.com> wrote:
> A bit off topic, but I have a question about async chan: how do you
> know how much buffer is optimal? What could it depend on
> theoretically? Or must it be arrived after much profiling and
> measurement for each individual program / communicating system?

Well, I think it requires some experience. It usually depends on such
parameters as expected input request rate in normal operating mode
(for example, for a web app we want to buffer requests for say 3
seconds, if it overflows we want to return "503 Service Temporarily
Unavailable"), memory consumption by a single message (we want to keep
them at least in main memory), number of hardware threads (more
threads -> more buffering), parameters of disk IO subsystem (we want
to keep spindles rotating), etc.

However, of the other hand, there is no way you do not choose chan
capacity. By not specifying it you just accepting the default. There
is no distinction between buffered and unbuffered channels, 0 as
magical constant as any other in this context. So if I completely have
no idea at the moment, I think I would do something along the lines
of:

//TODO: arbitrary constant, requires tuning
const InputChanCapacity = 42

...

inputChan := make(chan *Request, InputChanCapacity)

At least now it is explicit, and does not make the impression we don't
have the problem.

Dmitry Vyukov

unread,
Aug 23, 2011, 4:12:00 AM8/23/11
to Brad Fitzpatrick, Anh Hai Trinh, Kyle Lemons, golang-nuts
On Tue, Aug 23, 2011 at 7:58 AM, Brad Fitzpatrick <brad...@golang.org> wrote:
> On Tue, Aug 23, 2011 at 6:13 AM, Anh Hai Trinh <anh.ha...@gmail.com>
> wrote:
>>
>> A bit off topic, but I have a question about async chan: how do you
>> know how much buffer is optimal? What could it depend on
>> theoretically? Or must it be arrived after much profiling and
>> measurement for each individual program / communicating system?
>
> This has been bugging me ever since starting to use Go.
> I ask and the answer I receive is "oh, just make it a small buffer".  What
> is small?
> I keep hoping that Go will add a make(chan foo, buffered) where you say
> "bounded, but buffered and sane automatic size for throughput".

I doubt it is possible... I mean good automatic tuning, of course it
can export const Buffered = 32, but it makes little sense.


> Not having
> that, what I end up doing in all my packages where I want that is:
> const buffered = 32 // some arbitrary size
> ...
>    c := make(chan *Foo, buffered)

Looks quite reasonable to me.

unread,
Aug 23, 2011, 8:10:30 AM8/23/11
to golang-nuts
On Aug 22, 9:30 pm, Dmitry Vyukov <dvyu...@google.com> wrote:
> However, sometimes buffered channels do eliminate deadlocks.

In general, deadlocks are eliminated by examining the cause of the
deadlock. When the cause is understood, the programmer knows that the
program contradicts his/her intentions. This knowledge is a piece of
information as such. Based on this information, the programmer can fix
the program in various different ways. One possible way is: after
packaging the necessary bits of data into a single message, the
message is sent via an *unbuffered* channel from goroutine A to
goroutine B. The message contains control data which will tell the
receiving goroutine B what needs to be done in order for the program
as a whole to correctly perform its function.

> Namely, when capacity is
> larger or equal to number of messages sent (by single goroutine or all
> goroutines depending on the situation).

I agree with you if you mean this as an option.

In theory, nothing is preventing the programmer from resolving the
deadlock situation by utilizing synchronous/unbuffered channels only
(that is: by not using any buffered channels).

Program correctness does not depend on execution performance. If the
program is correct, slower or faster execution cannot be the cause of
a deadlock in the program (this is obvious, since there are no
deadlocks in correct programs). This also applies to distributed
correct programs spanning multiple machines.

> Sometimes we just need to send
> a single message and we know then chan capacity is >1.

We only know that chan capacity >1 is an option that solves an issue.
It isn't a necessity.

> Sometimes we
> know that N messages will be send and create chan with capacity N.

We only know that chan capacity N is an option that solves an issue.
It isn't a necessity.

roger peppe

unread,
Aug 23, 2011, 8:22:20 AM8/23/11
to Dmitry Vyukov, golang-nuts, Brad Fitzpatrick, Kyle Lemons, Anh Hai Trinh

In my experience, buffered channels are harder to reason about because the buffering adds greatly to the program's state space (look at the differing number of states when using Spin to analyse buffered vs unbuffered chans for evidence).

One example that's bitten me: RPC-style communication between goroutines. If the request channel.is synchronous, then when the send completes you know that the serving goroutine has received the request, so you know a reply is forthcoming. If you want to make one of two requests depending on which goroutine is available, that's easy. You can't do that with buffered channels.

Also if the values are large, a synchronous chan could be faster because it need copy the value only once, rather than into the buffer then out again. I think that this may be the reason the synchronous chans are currently faster when GOMAXPROCS=1.

Dmitry Vyukov

unread,
Aug 23, 2011, 9:15:56 AM8/23/11
to roger peppe, golang-nuts, Brad Fitzpatrick, Kyle Lemons, Anh Hai Trinh
On Tue, Aug 23, 2011 at 4:22 PM, roger peppe <rogp...@gmail.com> wrote:
> In my experience, buffered channels are harder to reason about because the
> buffering adds greatly to the program's state space (look at the differing
> number of states when using Spin to analyse buffered vs unbuffered chans for
> evidence).
> One example that's bitten me: RPC-style communication between goroutines. If
> the request channel.is synchronous, then when the send completes you know
> that the serving goroutine has received the request, so you know a reply is
> forthcoming. If you want to make one of two requests depending on which
> goroutine is available, that's easy. You can't do that with buffered
> channels.
>
> Also if the values are large, a synchronous chan could be faster because it
> need copy the value only once, rather than into the buffer then out again. I
> think that this may be the reason the synchronous chans are currently faster
> when GOMAXPROCS=1.

If message copying takes that long that it affects performance, then
it just should be passed by pointer (if it's expensive why we want do
even 1 copy?). So we can consider that messages are always small, and
for small messages buffered channels are faster. Here is what I see on
my machine:

runtime_test.BenchmarkChanProdCons0 10000000 244 ns/op
runtime_test.BenchmarkChanProdCons0-2 5000000 443 ns/op
runtime_test.BenchmarkChanProdCons0-4 2000000 553 ns/op
runtime_test.BenchmarkChanProdCons0-8 1000000 1233 ns/op
runtime_test.BenchmarkChanProdCons0-16 1000000 1205 ns/op
runtime_test.BenchmarkChanProdCons10 20000000 123 ns/op
runtime_test.BenchmarkChanProdCons10-2 5000000 284 ns/op
runtime_test.BenchmarkChanProdCons10-4 5000000 385 ns/op
runtime_test.BenchmarkChanProdCons10-8 2000000 783 ns/op
runtime_test.BenchmarkChanProdCons10-16 2000000 819 ns/op
runtime_test.BenchmarkChanProdCons100 20000000 92.5 ns/op
runtime_test.BenchmarkChanProdCons100-2 10000000 166 ns/op
runtime_test.BenchmarkChanProdCons100-4 5000000 385 ns/op
runtime_test.BenchmarkChanProdCons100-8 5000000 437 ns/op
runtime_test.BenchmarkChanProdCons100-16 5000000 379 ns/op

chris dollin

unread,
Aug 23, 2011, 9:21:49 AM8/23/11
to Dmitry Vyukov, roger peppe, golang-nuts, Brad Fitzpatrick, Kyle Lemons, Anh Hai Trinh
On 23 August 2011 14:15, Dmitry Vyukov <dvy...@google.com> wrote:

> If message copying takes that long that it affects performance, then
> it just should be passed by pointer (if it's expensive why we want do
> even 1 copy?).

(a) Using pointers means that things will (almost certainly) be up
for garbage-collection; using copies likely means that won't be
necessary.

(b) Using pointers means that the goroutines have to have a well-defined
protocol for when the pointess are accessed; using copies means
that they don't interfere one with another.

Yes?

Chris

--
Chris "allusive" Dollin

Dmitry Vyukov

unread,
Aug 23, 2011, 9:57:33 AM8/23/11
to chris dollin, roger peppe, golang-nuts, Brad Fitzpatrick, Kyle Lemons, Anh Hai Trinh

Why have I done that optimization?!1! If I would not do it, now I
would be able to say that sync channels do 2 copies as well :)
OK, it's a good point. I think it can affect performance of massages
of medium size. For example, for a message of size 256 one has to
choose between: (1) 1 copy and no dynamic allocation but no buffering
or (2) 2 copies and buffering but dynamic allocation...

roger peppe

unread,
Aug 23, 2011, 12:11:55 PM8/23/11
to Dmitry Vyukov, golang-nuts, Brad Fitzpatrick, Kyle Lemons, Anh Hai Trinh
On 23 August 2011 14:15, Dmitry Vyukov <dvy...@google.com> wrote:
> On Tue, Aug 23, 2011 at 4:22 PM, roger peppe <rogp...@gmail.com> wrote:
>> In my experience, buffered channels are harder to reason about because the
>> buffering adds greatly to the program's state space (look at the differing
>> number of states when using Spin to analyse buffered vs unbuffered chans for
>> evidence).
>> One example that's bitten me: RPC-style communication between goroutines. If
>> the request channel.is synchronous, then when the send completes you know
>> that the serving goroutine has received the request, so you know a reply is
>> forthcoming. If you want to make one of two requests depending on which
>> goroutine is available, that's easy. You can't do that with buffered
>> channels.
>>
>> Also if the values are large, a synchronous chan could be faster because it
>> need copy the value only once, rather than into the buffer then out again. I
>> think that this may be the reason the synchronous chans are currently faster
>> when GOMAXPROCS=1.
>
> If message copying takes that long that it affects performance, then
> it just should be passed by pointer (if it's expensive why we want do
> even 1 copy?). So we can consider that messages are always small, and
> for small messages buffered channels are faster. Here is what I see on
> my machine:

sure. it might not matter - i definitely consider the copying issue
a more minor point. my first point refers to correctness and
ease of reasoning and is IMHO a good reason for the default
channel buffer size to remain 0.

Kyle Lemons

unread,
Aug 23, 2011, 1:22:39 PM8/23/11
to Dmitry Vyukov, roger peppe, golang-nuts, Brad Fitzpatrick, Anh Hai Trinh
Here is what I see on
my machine:

runtime_test.BenchmarkChanProdCons0     10000000               244 ns/op
runtime_test.BenchmarkChanProdCons0-2    5000000               443 ns/op
runtime_test.BenchmarkChanProdCons0-4    2000000               553 ns/op
runtime_test.BenchmarkChanProdCons0-8    1000000              1233 ns/op
runtime_test.BenchmarkChanProdCons0-16   1000000              1205 ns/op
runtime_test.BenchmarkChanProdCons10    20000000               123 ns/op
runtime_test.BenchmarkChanProdCons10-2   5000000               284 ns/op
runtime_test.BenchmarkChanProdCons10-4   5000000               385 ns/op
runtime_test.BenchmarkChanProdCons10-8   2000000               783 ns/op
runtime_test.BenchmarkChanProdCons10-16  2000000               819 ns/op
runtime_test.BenchmarkChanProdCons100   20000000                92.5 ns/op
runtime_test.BenchmarkChanProdCons100-2 10000000               166 ns/op
runtime_test.BenchmarkChanProdCons100-4  5000000               385 ns/op
runtime_test.BenchmarkChanProdCons100-8  5000000               437 ns/op
runtime_test.BenchmarkChanProdCons100-16         5000000               379 ns/op

I think this benchmark is highly misleading in the context of this discussion.  I don't think anyone is arguing that with 1 producer and 1 consumer aiming for throughput, using a buffered channel won't speed things up.  This does *not* mean that they are "better" or a "more suitable" default.  In my opinion, this is a prime example of where we should encourage people to "make it correct, then make it fast."  Synchronous channels are easier to reason about and debug.
Reply all
Reply to author
Forward
0 new messages