Since almost all go code uses go routines and
unbuffered channels. You may not really thought very much about why they're
on buffered or what it means when you're actually transmitting or receiving
on a channel. That is unbuffered, but I think it's worth thinking
about it a little bit because Channel communication, when I'm buffered is
really both, passing a piece of information and explicit
synchronization just to illustrate that. Let's make a little program and look,
At how that synchronization works. So I'm going to make a program
that runs a go routine.
And that girl routine is going to loop around forever.
And provide numbers. So it's a very simple thing is going to be
ready to transmit. Let's make ourselves a channel.
Channel of integers. And here we've got to say down that channel.
I will send I and then I will add 1/2. I
So, this will Loop forever and it will output a an integer on
that channel and loop around ready to the river. Now, if
nobody is willing to receive on that channel, this does absolutely
nothing at all. It's sits right here on this line waiting.
So that's where a synchronization is happening. Has to have someone else ready to
read in order for that to work. So we can make another
simple thing here. We're going to say, okay, I'm going to read
A value here.
And then we go and we should be able to run that.
and we got zero because that was the first value and if we read it a few more times,
We'd get the next few values. So what's nice about
this is it's dead. Easy to think about this thing
is going nowhere until someone's ready to read. And this thing is going
nowhere until somebody is actually ready to write. And in general,
unbuffered channels make it very, very easy to reason about how your program is working because they will
stop operating go routines on the main part of the function
until it actually needs to transmit or read on that particular Channel and until two
people are willing to cooperate.
Evening, quite complicated systems where there many things are Reading Writing on channels. It's
very easy to understand what's happening. Now, an obvious thing you have to
do in programs is tell the program to stop.
So there's a couple of ways to do this one would be just to close the channel and
then it will stop being able to transmit or receive on it, but you can
get a Terrors that way because trying to write on a closed channel, will
generate an error. A typical way to do. This is actually to have another Channel used
for telling the program just to give
Give up. So let's the modifiers little bit or make ourselves
a channel. The channel doesn't actually have anything on it.
Just an empty struct song. And within here, we're going to do two
things.
I'm going to go in here, I'm going to say I'm ready to send that and what I've done and I'll update it.
I'm also ready to receive on the done Channel, and when that happens, this program
will terminate. So again, it's looping around
is adding one every time it's given out a number so you'll never give out the same number twice, but if it gets
told to terminate, it will terminate. And so we can do what we've
done here and read. And then we can do an explicit, send a
message.
And this case was going to send the empty empty struct. That's the definition, that's the actual
value, which is empty. And then let's see what happens if we try and
read on that channel. So that channel is still alive. But
this, this thing will have terminated is only choice once it's that has happened. And by
the way, if this happens that happens, that's
what the synchronization really means, you know, that, those two things have happened, so this
will absolutely have happened, so there's no chance for this to read. So let's just run.
And there you go. All go routines, are asleep. Deadlock, this could never receive. It
knows that there's actually nothing that could could receive on this.
So there has actually genuinely terminated now changing that to a
close on that channel will cause this case statement to terminate the
next time that we'd actually goes around, but you can
see from running this program that we've introduced a little bit of non-determinism
by run it.
That way, it worked, but occasionally, you'll get all go, routines are asleep, depending on which
happened, we've actually broken, that rule about the synchronization, they're the nice
thing about transmitting on that channel is your guarantee that somebody this guy had to be receiving
and therefore, you know, that the channel did actually terminated at
that point. So in general just sending something down a channel, you get that
synchronization point and you're absolutely sure what has happened.
All of the unbuffered channels I used here are
both read and write anyone can read and write to them. It's
sometimes useful to restrict that to read only channels and
right only channels now, they may seem like kind of a strange thing to have
and in fact you can take a read write Channel and pass it to
another function saying this is only readable or only writable
so I'm going to do a little bit of refactoring here and make this thing a bit more interesting and then show how to do
that.
At to prevent any sorts of concurrency mistakes. So, instead of having
all the stuff to find in Maine here, I'm going to pull this out and I'm going to
make myself a new
type, which is going to be this counter,
and it's going to have the channel in it.
And it's also going to have the done Channel.
And it's going to have integer and we'll make ourselves a little function to create one of
these.
so create one of these and just going to go
I can you counter and then we'll just make the channels within
it and he's going to be bi-directional. There's nothing in here that says there
anything other than normal buffered channels above
channels and I is set to 0 so we don't need to
That's that crazy new counter. So get one of these guys here.
Okay, so we've got a new counter here and going to want to be able to get
at a channel to read from, but I'm not going to return C. So I'm going
to have a method on this.
And we're going to say on the counter.
Go to the source. It's called I'm going to say we're going to channel
which you can only read from. So this is the Syntax for that.
So this little arrow here, so we can reach me and I'm just going to return
C dot C. I haven't got this.
I'm going to say
Seedot, get sauce.
And just change all this to read.
and,
I'm going to have another function here to stop it,
which is the thing that's going to send a message down is done Channel.
Okay. And the other thing we're going to watch to is when this guy starts is
start the magic go routine that does all the work.
It's like this. And now we have access to those channels are going to say
where we going to do that. And at one time,
and we're always running to
Receive on the done Channel.
And give up.
And then we go. So when you create a new account, you get this girl routine
running and you can terminate it by sending a message to their, which, in fact, we do using that.
And I think we're in good shape, let's give it a run through what I
forgot. Oops.
Yes, it's counter
and let's counter make sure that's correct
and good.
Very good and ways my done Channel.
Get rid of that and it still it to stop.
So same, same kind of situation here with the thing running and we tell it to
stop and obviously got the error because I tried to read 3.2 there.
But what you'll notice is that this channel here read, which is
actually the channel, That was originally defined, see which is defined here,
has been magically turned into a read-only channel and if I try to mess with it,
let's just suppose that in here, I decided to try to write to it.
You'll see that I get a a compile time error and you cannot write to us it's
going to be useful for controlling who has access to channels and can
in some circumstances make it easier when you're writing code to reason about it because you know,
okay only this person has access to go to write to it or read from it
in the counter example. I used a channel to signal that the counter go routine
should terminate. That was the done Channel and that works great. If you have a single go
routine but it's quite common that you might have many.
Routines and there might be a need to do some sort of termination of lots of them.
Now, you can share the done Channel, across, lots of goroutines, and then you
can close it or you can try and send a message to it. If you send a
message to her, only one of them's going to pick up because of synchronization. If you close that
channel, it acts a bit like a broadcast and everyone who was trying to read on it
will then stop reading what unable to read and their go routines will
terminate
Another way to do this is to wrap all that logic up into another type
and there's a rather. Nice example from the folks at Google were they invented, what they
called a context and that wraps, this kind of functionality and users read
only channels. So one of things I've done here is I've changed the counter
to use a context and context is really quite a simple thing
is just in this instance it just has a done Channel within it
and when you create a new one of them, you get a new contacts with a ready to go.
Channel and when you call stop on that context it closes that channel.
So calling stop on the context will tell you one who's using that context. They have to
terminate and anyone who's using that will use this get
done method to get the done Channel and rather than getting
access to it explicitly, they get access to a
read-only version of it. This prevents any other routine from deciding to close
that channel, which could cause problems. For example, you can't close the channel twice
so you tab it have.
An issue there by having a single place where it gets closed. Then you
could ensure that it's only closed once and by only handing out read
only channels, no one else can close it. And if someone tried then at compile time you get
an error. And then what I did was, I changed the counter structure
so that includes a context and that gets passed in when you create
the new counter and the cost is stored. And then
we just grab the done channel from the context and read from it as before.
Now, to make sure everything is clean and terminates nicely. I actually used a white group
from the sink package. This is one of the things in the sink package which is really nice. It allows
you to wait until some number of go routines. Have terminated
the way you do that is you declare a weight group and then every time you add a
go routine you call AD which adds to a counter and every
time a go routine terminates you call done which will decrement the counter here. I done
it using a defer for Simplicity and when you want to make sure everything is
done. You see it?
And do wait. So what this will do is will do exactly what happened before. It's going
to read three numbers from the read only channel from the counter.
Is then going to call the context and tell the context. Please, stop whatever is running out there
and then we'll just wait until things terminate and just to make it really clear. I added a printf in here
saying you know what the counter is now terminated. So if we run it
What we're going to see is we read those three numbers? We call contact, stop it closes
this channel here, done and the counter terminates. Everything is
cleaned up really nicely with this idea of a context. If you read
about Google's implementation, there are many other interesting details of what they pass around in a
context, but this basic idea is extremely powerful. So to illustrate, this concept
of a context where it closes a channel to Signal termination to a number of
different go routines. I've added something else to this program which
is another
Type which doubles something so it what it does is it goes
through and it reads from a an in
channel here. When you make one of these new doublers and it outputs
on an out Channel. This thing here, whatever that number is
doubled as you can see this slowly unfamiliar looking syntax here which is
saying, read on in double it and write it on out
and if we get a done termination then we will
terminate
that done Channel came from a context to
context was passed into the doubler. As what happens in the in Maine
is that I create a single context, use it to create a counter, get
that counters output Channel. I'm always giving 012
create a doubler, same contact pass it that channel. So it can now read whatever
the counter is creating get the double as output, which is now going to be
0 to 4 etcetera. And then read from it.
Close the context. I'm done close the channel Singletary. Body has stopped and
I use the weight group just so I can see clean termination from the program. And again
I put in a message here saying doubler terminated and I also have
counter terminated. So let's run this program and see what happens.
It seemed like it worked. Fine, we got some doubles. Obviously, those two things have been linked together,
we call the context stop and the program terminated. So the termination part is
working very, very nicely. It cleans up very well and you can imagine having many different
go routines waiting on that done Channel and you get a
context stop called and everything cleans up nicely and obviously the
program is terminated. So the weight group is actually finished which means we'll go routines,
are finished, so that's fantastic. However actually there's a really nasty Pitfall in this program if I
run it.
Again if I'm lucky I'm going to get an error, right? So as you can see, something
bad has happened. You see that the counter is terminated, but you can see
that the double it doesn't seem to. There's no message saying it and there's also an error
about everything having gone to sleep. The reason that happens is go back to that. Rather
ugly looking line here. If we think about this carefully, this can actually
create this deadlock situation. What can happen is we can read a
value in on D in
And then nobody's perhaps what willing to listen, Tandy
out to read something because in fact, we've terminated as it we're stuck
and so we have to change this little bit whenever you see this kind of thing you'd better watch out because this is it's
not simple to reason about because there's different synchronization points. So we're going to break
it up a little bit, so we're going to start by saying
I'm going to read a value.
Sighs, pretty clear. So either reader value or terminate and in the case I read a value,
then I'm willing to do one or two other things. I'm willing to provide that thing
doubled or I'm willing to
terminate
I'm going to just for the sake of this.
and there's go call this to just, so we can see where that terminated
So again, this makes the reasoning about the concurrency. Pretty simple. There's simple
cases here one. One receive or one send on a
channel on each line. There are these two particular instances. Once
I'm in here, I'm going to do one of those two things otherwise I'm not. So let's
just give this a run.
And you see that in different cases, the two different
terminations occurred. And if I add a return in here,
then it'll be nice and neat. You see the things terminating. So this
context idea, which Google came up with is a very neat way of passing
things around, but underneath is just channels, it's just closing a channel for
termination reasons and all it really does is prevent you accidentally
causing close twice on a channel. There's another way to do
that, of course, there is a lovely function called sink once.
So, let's just take a look at that in the go doc.
And sink once will call a particular function,
exactly once. And so, you can use that to close a Channel. Once,
although frankly, every time I've done it, it felt a bit like a hack. So I recommend
doing something like this.