- We've already seen how to join a single thread.
In other words, how to wait for that thread to finish.
You simply spawn the thread and get back the handle.
And then on the handle, you call the join function.
The join function waits for that thread to terminate
either successfully or unsuccessfully.
And then once the join method has returned,
it gives you back a result.
You can then either unwrap the result if you're confident
that it's worked correctly, or you can do a pattern matching
on the result to see if it was okay or if it was an error.
So that's how you wait for a single thread to finish.
What if you want to wait for multiple threads to finish?
Let's say you've kicked off five threads
and you want to wait for all of them to finish
before you continue.
So let's see how to do this.
To join multiple threads, you'd simply create a collection
to hold all the individual thread handles.
So it might look like this.
I've created a vector, it's mutuable
because I'm gonna be changing the state
of the items in here.
The vector contains joinHandles,
one joinHandle per thread.
From a technical perspective,
joinHandle is a generic structure.
So when you specify the vector as holding joinHandles,
you have to give it a type parameter here.
In fact, you don't have to give it a type parameter.
You can use an underscore to tell Rust I don't really care
what the type parameter's going to be.
Okay, so the compiler then just kind
of ignores the type parameter.
So what I've created here is an empty vector,
which will eventually contain the handles
of all the threads I've created.
So every time you spawn a thread,
add this handle into the collection like this.
In this example here, I've looped round five times.
Each time around the loop, I spawn another thread
and I get back its handle
and I just add that handle into my collection.
So this collection will contain the handles
for all five threads.
So on the main thread, you then go off
and do whatever work you want to do.
And at the point where you're finished
and you're then ready to just wait
for those other threads to finish,
what you do is just call join on all those thread handles,
literally just like this.
You write the loop to iterate
through the collection of handles and you basically wait
for each handle to terminate one by one.
So if we had five threads,
then this code would loop five times.
The first thread handle, we'd wait for that to finish
and then maybe unwrap its content or do a pattern match.
And then we loop around waiting
for the next thread to finish and so on.
So eventually, by the time this loop terminates,
all those five threads will have joined,
will have finished and we're done.
So the example is in lesson14_multithreading_concurrency.
We'll look in main.rs first as usual.
And then the code we're gonna look at in particular,
demo_joining_thread_multiple.
And then obviously, we'll run the program.
I'm gonna run it several times
with different scenarios again.
So should we take a look at the code?
Here's the project, and here's my main code
in my main code at demo_joining_thread_multiple.
There we go and it's here.
Right, so let me run you through the code
that we have in this example.
Here is my collection,
my vector that's going to hold all my join handles
for all the threads I'm going to create.
I loop around five times.
Each time on the loop, I spawn another thread.
So without looking at the details
of the closure for each thread just yet.
And because that's kind of secondary really,
what it does inside doesn't really
of our concern on the outside.
We'll have a look at that in a minute.
The main thing is that we spawn the thread.
We'll get back a handle for that thread.
We take that handle,
we push it into our collection of all handles.
So our collection of all handles
will now contain five JoinHandles.
In my main code,
I'm going to loop around a few times being busy.
So whilst these background threads are doing their thing,
my main thread is also being busy in its foreground task.
It's gonna loop around five, six times, display a message
and sleep for half a second, 500 milliseconds.
So that's gonna take three seconds to complete.
That's not very long really.
And then we'll wait for those other threads to terminate.
So the main thread will wait
for those other threads to finish.
So we loop around all the handles.
For each handle, the join function
basically means wait, doesn't it?
Wait for that other thread to terminate?
I haven't got any panics in my other threads.
So there's not going to be any error conditions
that we need to worry ourselves about.
So it's okay to unwrap the result and just move on.
So should we take a closer look
at what's actually gonna happen?
Because five times I'm going to be spawning a new thread.
So I'm gonna basically run this closure five times
and I've got an element of randomness
to make the example a little bit less predictable.
Okay, so rand, I'm not sure if we've seen that before.
The rand crate, which I've imported here,
the rand crate not part of the standard crate.
It's a separate crate, separate dependency.
So I've imported
or I've specified the rand dependency, which is good.
And then in the rand crate,
there's a thread random number generator.
So that's a function
and it'll give you back a random number generator.
And then upon that random number generator,
you can ask it to generate a number in a specified range.
Generate the number in the range between five and 10.
So that would be each time I spawn another thread.
So I'm gonna do this five times.
Each time I spawn another thread,
I'm gonna get a different random number each time.
So each thread will have a different random number
and it uses that to sleep for a random amount of time.
So each thread will sleep for a random amount of time,
between five seconds and 10 seconds.
And of course, that's happening concurrently.
It's not consecutive.
These threads are running or sleeping at the same time.
So in total, it's probably gonna take maximum 10 seconds
and a bit for this application to terminate.
If the longest that a thread has to wait is 10 seconds,
the other threads will also be spinning at the same time.
So that random number between five and 10,
that's the length of time I sleep for.
This thread will sleep for that number of seconds.
And it says when it goes to sleep with its id
and it says when it wakes up.
So we're gonna see pairs of messages appearing
on the screen.
Thread number two has started to sleep,
thread number three,
thread number four, thread number five.
Oh, thread number two has finished its sleep.
So threads kind of enter and then exit the sleep state.
And that's it.
Once the thread has finished sleep then and woken up,
then it terminates.
And at that point, its join handle gets signaled
to say I've finished.
And eventually, our main code down here will have waited
for all five threads to terminate.
So I feel like we should run this now.
Okay, so you can see those background threads
have started their wait.
The main thread was busy for three seconds,
displaying 101 to 105.
Thread number one then entered a wait state,
waiting for those other threads to finish.
And those other threads,
they kind of finished in a random order.
Thread number two was quite late to finish.
Thread number three was quite soon.
Thread six was the soonest
because each thread had a different random sleep amount.
So you can see that the first join handle to terminate
was thread number...
Oh, this, what I should actually say
is this is the print statement here.
So that's the print statement to say thread six,
which started at sleep here,
and thread six was gonna sleep for five seconds.
That's the shortest compared to all the other threads.
All the other threads are longer than that.
So ThreadId six started sleeping
for six seconds, for five seconds I should say.
So that would be the first thread to terminate here.
And then the other threads all terminate.
And my main code, which is waiting
for all those handles to be released effectively,
when all five threads are finished, it'll realize
that it's time for the application to terminate.