Channels may not be the best solution in Go

371 views
Skip to first unread message

Travis Keep

unread,
Oct 3, 2019, 1:06:52 AM10/3/19
to golang-nuts
I've been working on a math library github.com/keep94/gomath.  This library has functions for generating prime numbers, ugly numbers, harshad numbers, happy numbers etc.  Since there are infinitely many prime numbers, ugly number, harshad numbers etc, these functions I wrote returned either a chan int64 or a chan *big.Int. However I just recently rewrote all these functions to return streams instead of channels.  By streams, I mean interfaces that have a Next method that returns the next value in the stream. In this post I explain why I made this change.

First off there is overhead for callers with channels. When my functions returned channels, they had to accept a Context since callers could never exhaust the returned channels. Callers would have to remember to cancel the context when they were done with the returned channel or else they would leak a goroutine.  

Second using streams is about four times faster than using channels. I don't know exactly why this is but I figure there is overhead involved in running a goroutine to feed the returned channel as well as implicit locking that has to be done when the channel is read from or written to.

Third, streams are less taxing on the GC than channels.  A chan *big.Int has to emit a newly allocated big.Int off the heap each time.  The Next method of a *big.Int stream can accept a *big.Int from the caller and write the next value to this caller supplied *big.Int instead of having to allocate a new *big.Int each time.

So at least for handling data structures handling an infinite number of integers or *big.Int, using streams with a Next method is better than using channels.


burak serdar

unread,
Oct 3, 2019, 1:24:20 AM10/3/19
to Travis Keep, golang-nuts
On Wed, Oct 2, 2019 at 11:07 PM Travis Keep <kee...@gmail.com> wrote:
>
> I've been working on a math library github.com/keep94/gomath. This library has functions for generating prime numbers, ugly numbers, harshad numbers, happy numbers etc. Since there are infinitely many prime numbers, ugly number, harshad numbers etc, these functions I wrote returned either a chan int64 or a chan *big.Int. However I just recently rewrote all these functions to return streams instead of channels. By streams, I mean interfaces that have a Next method that returns the next value in the stream. In this post I explain why I made this change.
>
> First off there is overhead for callers with channels. When my functions returned channels, they had to accept a Context since callers could never exhaust the returned channels. Callers would have to remember to cancel the context when they were done with the returned channel or else they would leak a goroutine.
>
> Second using streams is about four times faster than using channels. I don't know exactly why this is but I figure there is overhead involved in running a goroutine to feed the returned channel as well as implicit locking that has to be done when the channel is read from or written to.

It can be argued that you were misusing channels. Channels are
synchronization mechanisms between goroutines. They are not generic
data pipes. I think the operations you describe can be implemented as
a struct:

type PrimeGenerator struct {
// internal state for generator
}

func (p *PrimeGenerator) Next() *big.Int {...}

If you want, you can still add:

type Generator interface {
Next() *big.Int
}

>
> Third, streams are less taxing on the GC than channels. A chan *big.Int has to emit a newly allocated big.Int off the heap each time. The Next method of a *big.Int stream can accept a *big.Int from the caller and write the next value to this caller supplied *big.Int instead of having to allocate a new *big.Int each time.

Note that this can be dangerous. If the returned pointer is saved
somewhere else in the program as part of the internal state,
overwriting that instance of big.Int will also modify that state. You
get into issues about who owns data, which is never easy to solve.

>
> So at least for handling data structures handling an infinite number of integers or *big.Int, using streams with a Next method is better than using channels.
>
>
> --
> 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/538038ca-d6b9-4f9c-a69f-510284fc3ab8%40googlegroups.com.

JuciÊ Andrade

unread,
Oct 3, 2019, 8:17:51 AM10/3/19
to golang-nuts
On Thursday, October 3, 2019 at 2:24:20 AM UTC-3, burak serdar wrote:
It can be argued that you were misusing channels. Channels are
synchronization mechanisms between goroutines. They are not generic
data pipes.

I use channels exactly that way and they work pretty well.

burak serdar

unread,
Oct 3, 2019, 9:47:27 AM10/3/19
to JuciÊ Andrade, golang-nuts
Then you're paying some penalty for synchronization where the same
thing can be achieved without that penalty.

>
> --
> 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/8be28087-29d9-4544-8a1d-d6108b95c4b3%40googlegroups.com.

JuciÊ Andrade

unread,
Oct 3, 2019, 10:39:51 AM10/3/19
to golang-nuts
On Thursday, October 3, 2019 at 10:47:27 AM UTC-3, burak serdar wrote:
On Thu, Oct 3, 2019 at 6:18 AM JuciÊ Andrade <oju...@gmail.com> wrote:
> I use channels exactly that way and they work pretty well.

Then you're paying some penalty for synchronization where the same
thing can be achieved without that penalty.


Yes, indeed, but channels offer some significant advantages:

1. a division of labor between several goroutines is made possible without much effort. That can mean a lot, depending on how much complex is the logic involved in generating each value;
2. a possibly very complex state is preserved between each generated value (the execution stack);

I would dare to say that a prime number generator is more of an exception, because the state to be preserved between calls is well understood. Now think about some evolving code, where you can't know beforehand how complex that will be some years from now. Channels are a safer bet.

Anyway, you are correct: each use must be evaluated in it's pros an cons. You will see that in the vast majority of cases channels performance is more than enough.

burak serdar

unread,
Oct 3, 2019, 10:53:24 AM10/3/19
to JuciÊ Andrade, golang-nuts
On Thu, Oct 3, 2019 at 8:40 AM JuciÊ Andrade <oju...@gmail.com> wrote:
>
> On Thursday, October 3, 2019 at 10:47:27 AM UTC-3, burak serdar wrote:
>>
>> On Thu, Oct 3, 2019 at 6:18 AM JuciÊ Andrade <oju...@gmail.com> wrote:
>> > I use channels exactly that way and they work pretty well.
>>
>> Then you're paying some penalty for synchronization where the same
>> thing can be achieved without that penalty.
>>
>
> Yes, indeed, but channels offer some significant advantages:
>
> 1. a division of labor between several goroutines is made possible without much effort. That can mean a lot, depending on how much complex is the logic involved in generating each value;

When there are multiple goroutines involved, you have to use channels.

> 2. a possibly very complex state is preserved between each generated value (the execution stack);

This I disagree. A list achieves the same purpose without
synchronization overhead.

>
> I would dare to say that a prime number generator is more of an exception, because the state to be preserved between calls is well understood. Now think about some evolving code, where you can't know beforehand how complex that will be some years from now. Channels are a safer bet.

Honestly, I don't understand this argument. Complexity of sequential
code has got nothing to do with concurrent behavior. If there is no
concurrency, then there is no need to use channels.

>
> Anyway, you are correct: each use must be evaluated in it's pros an cons. You will see that in the vast majority of cases channels performance is more than enough.
>
> --
> 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/eb84a84c-be8d-4b81-8c5c-33cb09b70168%40googlegroups.com.

JuciÊ Andrade

unread,
Oct 3, 2019, 1:58:57 PM10/3/19
to golang-nuts
Burak, feel free to correct me if I am wrong, but now I think I understood the heart of the matter:

Your approach to software development is different from mine. Nothing wrong with that.

. you normally write sequential code, and uses concurrent code where it fits best. That is fine.
. I use to write concurrent code, and use sequential code where it fits best. That is fine as well.

Concurrency mechanisms in Go are so easy to use that it allows me to take that approach.
With a little bit of caution to not create a big ball of mud[1], you can write clean concurrent code.
You said there is a synchronization overhead when a program uses channels. That is true.
On the other hand, when we divide the load among several cores we end up with a net gain.
Depending on the task at hand the difference can be staggering! I mean 15x faster or more!

If we consider that nearly all CPU is multicore these days [2], we will soon conclude that writing concurrent code even for simple tasks makes sense, in order to leverage that processing power.

Your concurrent code will run keep running well in newer CPUs. Your single threaded code won't.

Again, nothing wrong with your approach, ok? To each its own.





Robert Engels

unread,
Oct 3, 2019, 3:21:46 PM10/3/19
to JuciÊ Andrade, golang-nuts
A point to consider though, is that often sequential code is used and concurrency is achieved by running multiple processes. This is pretty much the design of the cloud. A top-level coordinator partitions the work-load, and sequential programs process it, and the top-level assembles the results - think map/reduce at scale. Often the complexity of concurrency within a process is not needed (or is completely hidden away).

--
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.

burak serdar

unread,
Oct 3, 2019, 3:45:30 PM10/3/19
to JuciÊ Andrade, golang-nuts
On Thu, Oct 3, 2019 at 11:59 AM JuciÊ Andrade <oju...@gmail.com> wrote:
>
> Burak, feel free to correct me if I am wrong, but now I think I understood the heart of the matter:
>
> Your approach to software development is different from mine. Nothing wrong with that.
>
> . you normally write sequential code, and uses concurrent code where it fits best. That is fine.
> . I use to write concurrent code, and use sequential code where it fits best. That is fine as well.
>
> Concurrency mechanisms in Go are so easy to use that it allows me to take that approach.
> With a little bit of caution to not create a big ball of mud[1], you can write clean concurrent code.
> You said there is a synchronization overhead when a program uses channels. That is true.
> On the other hand, when we divide the load among several cores we end up with a net gain.
> Depending on the task at hand the difference can be staggering! I mean 15x faster or more!
>
> If we consider that nearly all CPU is multicore these days [2], we will soon conclude that writing concurrent code even for simple tasks makes sense, in order to leverage that processing power.
>
> Your concurrent code will run keep running well in newer CPUs. Your single threaded code won't.

Not all programs benefit from concurrency. Writing concurrent code for
essentially sequential programs will not benefit from multiple cores,
like generating prime numbers. Do not forget that concurrency includes
overhead for context switch and memory barriers. Using channels in a
sequential program is I think misuse of channels. There is no
performance gain in having a sequence of goroutines, each waiting for
the previous one the complete.


>
> Again, nothing wrong with your approach, ok? To each its own.
>
> [1] http://laputan.org/mud/
> [2] https://www.amd.com/en/products/cpu/amd-epyc-7742
>
>
>
>
> --
> 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/41a107de-e426-456a-abab-d0b058e39373%40googlegroups.com.

Michael Jones

unread,
Oct 4, 2019, 9:57:08 AM10/4/19
to burak serdar, JuciÊ Andrade, golang-nuts
Travis, glad to hear that you are exploring Harshad Numbers. It is an area where I have done more than a decade of work and I did not know that anyone else even cared about them. If you ever want to know how many thousand digit (or whatever) base 10 (or whatever) numbers have the Harshad property, and of those, how many of their digits are 3 or 11 (or whatever), let me know. I have a Go-language program that uses channels that can answer the question. ;-)

Michael



--
Michael T. Jones
michae...@gmail.com

Jake Montgomery

unread,
Oct 4, 2019, 11:33:47 AM10/4/19
to golang-nuts
JuciÊ, I'm not going to endorse or dispute you assertions in the general case.

But, I do want to point out that the OP started with "I've been working on a math library". It then provides a link to the library on github. It may be a matter of personal choice and style when writing an application, or code for private use. However, when one is writing a library intended for public use, a good programmer needs to consider things a more deeply. In the specific case outlined in the OP, I believe providing simple blocking function is the best approach. If the library returns a channel, it is imposing the channel overhead, as well as adding unnecessary goroutines, and potential complexity, to the code of the library user. The library user may not want or need concurrency for their specific use case, but they are stuck with it. On the other hand, If only a blocking function is provided, it is few trivial lines of code to funnel that into a channel if the user wants to.

IMHO, in the OP's case it sounds like a blocking function is the correct API for their library. Of course, if they want to provide a channel version in addition, that's fine.

Rob Pike

unread,
Oct 5, 2019, 2:44:39 AM10/5/19
to burak serdar, JuciÊ Andrade, golang-nuts

Not all programs benefit from concurrency. Writing concurrent code for
essentially sequential programs will not benefit from multiple cores,
like generating prime numbers. Do not forget that concurrency includes
overhead for context switch and memory barriers. Using channels in a
sequential program is I think misuse of channels. There is no
performance gain in having a sequence of goroutines, each waiting for
the previous one the complete.

You are conflating concurrency and parallelism, or perhaps the problem and the solution. Some programs (solutions) benefit from expression as concurrent code, others do not. Whether performance improves depends on how many cores you have as well as the inherent nature of the problem. Remember there is no such thing as parallelism on a single core, yet the idea of using concurrency in programs arose when multicore was but a dream.

Judging the benefit of an approach to a problem depends on your figure of merit. Your figure of merit seems to be performance, which is perfectly valid. Mine depends on the problem and on the solution, and performance may not be the most important component.

In other words, some solutions work well expressed concurrently; others do not. Whether the performance improves is only one part of deciding how well. One programmer's misuse may be another's thing of joy.

-rob

Reply all
Reply to author
Forward
0 new messages