- Now that we have our signaling semantics in place,
what I want to share with you is the three basic
channel patterns that everything else is built on top of.
Then we're gonna look at more advanced channel patterns.
I cannot teach you how to be a expert multi-threaded
software developer, this takes time and practice.
What I can teach you are all the basic mechanics
and semantics and continue to stress to you
don't add this complexity until you need it.
I wanna go through these basic channel patterns.
We'll go through some higher level patterns,
and then we'll start looking at more code
as we continue through concurrency and tooling
that leverages everything here.
I would almost make an argument that if you're doing
something that goes against the grain of these patterns
I'm gonna show you doesn't necessarily mean it's wrong,
but you're probably adding a lot of extra complexity
to your software that maybe ya don't need.
There are exceptions to everything,
but maybe you don't need.
This is the very first basic channel pattern
around signaling, and it's called wait for task.
Wait for task pattern is like base pattern that we're gonna
use for higher level patterns like goroutine pooling.
We'll leverage a wait for task type of pattern.
Any time we look at this type of code,
what we wanna do is, I like thinking about goroutines
as people, it allows me to visualize the orchestrations
that are going on and I always like the idea
of a manager and an employee.
There's somebody who's in charge and somebody
who's doing work for that person's in charge.
Cause usually if we're doing signaling, it's because
we want someone on our behalf to do something for us,
either while we can do other things or so we can potentially
stop or correct that person if things are not going right.
This is our wait for task.
We're gonna see it again when we go to some higher level
patterns like pooling, and this is the way it works.
The very first line of code you see on line 34
is the built-in function make.
Remember, we can use make for slices,
we can use make for maps, and we use it for channels,
those three particular reference types.
There's no other way to initialize a channel and put
it into its open state other than make,
because you can't pre-initialize data inside of a channel
during construction, so literal construction doesn't exist.
When we make a channel, we're gonna use the keyword chan,
and then the type of data that we're gonna
be using through this channel.
Data's always typed in this language,
so our channel's also typed.
What you're seeing here is an unbuffered channel.
I know it's unbuffered because I don't have a number here.
There's no number.
This would be a buffer channel of 10,
but we don't have that,
this is a unbuffered channel.
But, what I'm really gonna look at
on line 34 is the following.
When I see this make call, what I see is we are creating
a channel where we want guarantees that the signaling
sending is being received.
Want guarantees that the send has been received
and, right now, we're gonna also signal with data
where the data is string.
You see, I'm putting everything within the scope
of the semantics that we talked about.
Moving away from the syntax and moving into the semantics.
This code is saying we want guarantees
and we're gonna signal with string data.
Perfect.
The channel is nothing more than a pointer variable
to a very complex data structure underneath.
Again, we're gonna be using value semantics all the time.
Wait for task.
Here's the idea.
I'm gonna go recruit some human being, some employee,
to go ahead and so some work for me,
and that's representative of the literal function
and the function call-in in the keyword go.
Here it is again.
I've gone off and I've launched a goroutine to do some work.
We could imagine the following scenario.
I want you to always consider yourself to be the manager.
This is our path of execution.
Here it is, here's the manager, and now we go off and we say
we want an employee to go help us do something.
Here we are, and we go launch a second
path of execution right here, on line 36.
This is our goroutine right now that's executing
the function, that's us, and now this is
the employee goroutine on line 36.
I want you to look on line 37.
On line 37, what you see is a channel receive,
it's a unary operation.
The operator arrow is attached to the channel variable.
Unary operation, and this is a channel receive,
so what we have right now is this receive
on our channel, and this is a blocking call,
because we want guarantees.
Basically, we're blocked right here.
We're basically saying hey, employee, come here.
Let's get involved.
Here you are, you now exist, you're moving.
But I need you to wait.
We're basically saying here is wait
for us to tell you what to do.
How long is the employee gonna wait?
It is unknown.
Remember, we have unknown latency here,
because we are going after guarantees.
We wanna guarantee that this employee here
is going to have received our signal.
You wait, you don't know what to do yet.
I'm gonna send it to you, you wait.
That means that at this point, this goroutine is blocked,
waiting for us.
In the meantime, we get to continue to run in parallel
and what we do is we get to line 41.
Line 41 is simulating some work that we have to do.
This is on line 41.
Some work that we have to do to prepare
the work for our employee.
How long are we gonna wait on line 41?
We don't know.
I've even tried to simulate the unknown latency here
with the time.Sleep function, and a random
number generator for duration.
We have no idea.
They have no idea how long they're gonna wait for us,
cause we have no idea how long this work is going to take.
Eventually, we get past line 41,
and now we get to our signaling.
Our send side of the signaling, which is right here.
Notice sending is a binary operation.
Receiving is a unary operation.
Then it's binary, cause we're gonna signal
with data, with string data.
Our work is now prepared and now what we're going to do
is we're gonna take this work that we have here
and we're gonna be sending it.
We're gonna send it to this other employee.
They're waiting right here on the receive.
We're gonna do the send.
Notice, the syntax looks like this.
It's the channel and it's the same arrow operator
where we're showing that he data is getting placed
into the channel.
We're gonna do the sending here.
Remember, we wanted guarantees.
This is blocking, waiting for corresponding receive.
This receive is in place.
Now what we have here is the send.
With the send in place now, we come here and what we now
have is the send and the receive together, finally.
Again, to get the guarantee, the receive happens
nanoseconds before the send.
But we're both kind of in a blocking situation here.
We are.
This goroutine is now in a block state, in a wait state.
This has been in a wait state and the only way
to keep moving now is to let this channel operation finish.
What we're gonna see here is that the receive
is gonna happen nanoseconds before,
and the receive is gonna start first.
This is gonna happen first
and it's gonna be able to move on.
Once the receive is done, then we're gonna move on second.
This is gonna happen first, that's gonna happen second.
Nanoseconds.
We're gonna get, when we get to line 43, is we're gonna get
a guarantee that the receive has happened.
Once this goroutine gets the data that we're sending
over this channel, they get to do their thing.
Right now, we're just displaying information
and they finish, and then we just move on,
and we're running in parallel.
This stuff gets confusing because the only thing
that's atomic is this operation here.
That is the only thing that's atomic.
Understand that.
Excuse me.
I've got two goroutines.
Goroutine One, Goroutine Two,
each running on their own core in parallel.
They both are put into this wait state when we get here,
because they've gotta have the guarantee.
That's what we asked for.
After that, these two goroutines are running
independent of each other on their own thread
from an operating system perspective.
What does that mean?
It means that these print statements
can technically happen in any order.
There's no guarantee that the print statement on Line 38
is gonna happen before the print statement on line 43,
because the only thing that's blocked here,
within nanoseconds of a time, is the channel operation.
This is why it makes it very hard to use print statements
to determine order, because the only thing guaranteed
is the receive is happening before the send.
If I try to build this program just running wait for task,
which I've set up here, you can see on this run,
it's showing you that the send happened before the receive.
If we don't realize what I just told you,
that the receive happens before the send, nanosecond,
we would think by the output that the send
happened before the receive, but that's not what happened.
Because this is only happening
within nanoseconds of time apart.
Both goroutines really kind of start just running
again at the same time.
If I ran this a few more times, we might get lucky
and see the receive output happen before the send.
There's no guarantees that this will happen,
but the fact that we're seeing, ah there it is right there.
Beautiful, look right there.
Suddenly, now, the receive looks like
it happened before the send (laughing).
This is why so many people get confused with concurrency,
because they're looking at the order of things and the only
order is atomically at that actual send and receive.
After that, all bets are off.
You cannot be using print statements to look at ordering.
I've left the print statements
in here to prove that to you.
When you're working with and learning concurrency,
remember your print statements are not gonna help you.
They're gonna really confuse you.
This is our wait for task.
Again, we're gonna use this type of pattern for pooling.