Skip to content

Commit 524a2f7

Browse files
jonathanpallantlistochkin
authored andcommitted
Added a multi-threaded-mailbox.
1 parent 9feed18 commit 524a2f7

File tree

12 files changed

+675
-1
lines changed

12 files changed

+675
-1
lines changed

build.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ pushd connected-mailbox
1313
cargo test
1414
cargo fmt --check
1515
popd
16+
pushd multi-threaded-mailbox
17+
cargo test
18+
cargo fmt --check
19+
popd
1620
popd
1721

1822
# Only build the templates (they will panic at run-time due to the use of todo!)
@@ -41,5 +45,5 @@ cp -r ./exercise-templates "${OUTPUT_NAME}/"
4145
rm -rf "${OUTPUT_NAME}/exercise-templates/target"
4246
cp -r ./exercise-solutions "${OUTPUT_NAME}/"
4347
rm -rf "${OUTPUT_NAME}/exercise-solutions/target"
44-
rm -rf "${OUTPUT_NAME}/exercise-solutions/connected-mailbox/target"
48+
rm -rf "${OUTPUT_NAME}"/exercise-solutions/*/target
4549
zip -r "${OUTPUT_NAME}.zip" "${OUTPUT_NAME}"

exercise-book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
- [Shapes](shapes.md)
1919
- [Connected Mailbox](./connected-mailbox.md)
20+
- [Multithreaded mailbox](./multi-threaded-mailbox.md)
2021

2122
# Async Rust
2223

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Multi-Threaded Mailbox Exercise
2+
3+
In this exercise, we will take our "Conneted Mailbox" and make it multi-threaded. A new thread should be spawned for every incoming connection, and that thread should take ownership of the `TcpStream` and drive it to completion.
4+
5+
## After completing this exercise you are able to
6+
7+
- spawn threads
8+
9+
- convert a non-thread-safe type into a thread-safe-type
10+
11+
- lock a Mutex to access the data within
12+
13+
## Prerequisites
14+
15+
- A completed "Connected Mailbox" solution
16+
17+
## Tasks
18+
19+
1. Use the `std::thread::spawn` API to start a new thead when your main loop produces a new connection to a client. The `handle_client` function should be executed within that spawned thread. Note how Rust doesn't let you pass `&mut VecDequeue<String>` into the spawned thread, both becuase you have multiple `&mut` references (not allowed) and because the thread might live longer than the `VecDeque` (which only lives whilst the `main()` function is running, and `main()` might quit at any time with an early return or a break out of the connection loop).
20+
21+
2. Convert the `VecDeque` into a `Arc<Mutex<VecDequeue>>` (use `std::sync::Mutex`). Change the `handle_client` function to take a `&Mutex<VecDeque>`. Clone the Arc handle with `.clone()` and `move` that cloned handle into the new thread. Change the `handle_client` function to call `let mut queue = your_mutex.lock().unwrap();` whenever you want to access the queue inside the Mutex.
22+
23+
3. Convert the `Arc<Mutex<VecDeque>>` into a `Mutex<VecDeque>` and introduce scoped threads with `std::thread::scope`. The `Mutex<VecDeque>` should be created outside of the scope (ensure it lives longer than any of the scoped threads), but the connection loop should be inside the scope. Change `std::thread::spawn` to be `s.spawn`, where `s` is the name of the argument to the scope closure.
24+
25+
At every step (noting that Step 1 won't actually work...), try out your program using a command-line TCP Client: you can either use `nc`, or `netcat`, or our supplied `tools/tcp-client` program.
26+
27+
## Optional Tasks:
28+
29+
- Run `cargo clippy` on your codebase.
30+
- Run `cargo fmt` on your codebase.
31+
32+
## Help
33+
34+
### Making a Arc, containing a Mutex, containing a VecDeque
35+
36+
You can just nest the calls to `SomeType::new()`...
37+
38+
<details>
39+
<summary>Solution</summary>
40+
41+
```rust
42+
use std::collections::VecDeque;
43+
use std::sync::{Arc, Mutex};
44+
45+
fn main() {
46+
// This type annotation isn't required if you actually push something into the queue...
47+
let queue_handle: Arc<Mutex<VecDeque<String>>> = Arc::new(Mutex::new(VecDeque::new()));
48+
}
49+
```
50+
51+
</details>
52+
53+
### Spawning Threads
54+
55+
The `std::thread::spawn` function takes a closure. Rust will automatically try and borrow any local variables that the closure refers to but that were declared outside the closure. You can put `move` in front of the closure bars (e.g. `move ||`) to make Rust try and take ownership of variables instead of borrowing them.
56+
57+
You will want to clone the `Arc` and move the clone into the thread.
58+
59+
<details>
60+
<summary>Solution</summary>
61+
62+
```rust
63+
use std::collections::VecDeque;
64+
use std::sync::{Arc, Mutex};
65+
66+
fn main() {
67+
let queue_handle = Arc::new(Mutex::new(VecDeque::new()));
68+
69+
for _ in 0..10 {
70+
// Clone the handle and move it into a new thread
71+
let thread_queue_handle = queue_handle.clone();
72+
std::thread::spawn(move || {
73+
handle_client(&thread_queue_handle);
74+
});
75+
76+
// This is the same, but fancier. It stops you passing the wrong Arc handle
77+
// into the thread.
78+
std::thread::spawn({ // this is a block expression
79+
// This is declared inside the block, so it shadows the one from the
80+
// outer scope.
81+
let queue_handle = queue_handle.clone();
82+
// this is the closure produced by the block expression
83+
move || {
84+
handle_client(&queue_handle);
85+
}
86+
});
87+
}
88+
89+
// This doesn't need to know it's in an Arc, just that it's in a Mutex.
90+
fn handle_client(locked_queue: &Mutex<VecDeque<String>>) {
91+
todo!();
92+
}
93+
}
94+
```
95+
96+
</details>
97+
98+
## Locking a Mutex
99+
100+
A value of type `Mutex<T>` has a `lock()` method, but this method can fail if the Mutex has been poisoned (i.e. a thread panicked whilst holding the lock). We generally don't worry about handling the poisoned case (because one of your threads has already panicked, so the program is in a fairly bad state already), so we just use `unwrap()` to make this thread panic as well.
101+
102+
<details>
103+
<summary>Solution</summary>
104+
105+
```rust
106+
use std::collections::VecDeque;
107+
use std::sync::{Arc, Mutex};
108+
109+
fn main() {
110+
let queue_handle = Arc::new(Mutex::new(VecDeque::new()));
111+
112+
let mut inner_q = queue_handle.lock().unwrap();
113+
inner_q.push_back("Hello".to_string());
114+
println!("{:?}", inner_q.pop_front());
115+
println!("{:?}", inner_q.pop_front());
116+
}
117+
```
118+
</details>
119+
120+
## Creating a thread scope.
121+
122+
The
123+
124+
<details>
125+
<summary>Solution</summary>
126+
127+
```rust
128+
use std::collections::VecDeque;
129+
use std::sync::Mutex;
130+
131+
fn main() {
132+
let locked_queue = Mutex::new(VecDeque::new());
133+
134+
std::thread::scope(|s| {
135+
for i in 0..10 {
136+
let locked_queue = &locked_queue;
137+
s.spawn(move || {
138+
let mut inner_q = locked_queue.lock().unwrap();
139+
inner_q.push_back(i.to_string());
140+
println!("Pop {:?}", inner_q.pop_front());
141+
});
142+
}
143+
});
144+
}
145+
```
146+
147+
</details>
148+
149+
### Solution
150+
151+
If you need it, we have provided a [complete solution](../../exercise-solutions/multi-threaded-mailbox) for this exercise.

exercise-solutions/multi-threaded-mailbox/Cargo.lock

Lines changed: 79 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[workspace]
2+
members = ["simple-db", "with-arc", "with-scoped-threads"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
authors = ["Ferrous Systems"]
3+
edition = "2021"
4+
name = "simple-db"
5+
version = "0.1.0"
6+
7+
[dependencies]
8+
thiserror = "1.0"

0 commit comments

Comments
 (0)