1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-06-25 02:06:46 +02:00

Update Concurrency course with times (#2007)

As I mentioned in #1536:

* Break into segments at approximately the places @fw-immunant put
breaks
 * Move all of the files into `src/concurrency`
 * Add timings and segment/session metadata so course outlines appear

There's room for more work here, including some additional feedback from
@fw-immunant after the session I observed, but let's do one step at a
time :)
This commit is contained in:
Dustin J. Mitchell
2024-04-23 09:26:41 -04:00
committed by GitHub
parent a03b7e68e5
commit face5af783
58 changed files with 385 additions and 246 deletions

View File

@ -0,0 +1 @@
# Exercises

View File

@ -0,0 +1,113 @@
---
minutes: 30
---
# Broadcast Chat Application
In this exercise, we want to use our new knowledge to implement a broadcast chat
application. We have a chat server that the clients connect to and publish their
messages. The client reads user messages from the standard input, and sends them
to the server. The chat server broadcasts each message that it receives to all
the clients.
For this, we use [a broadcast channel][1] on the server, and
[`tokio_websockets`][2] for the communication between the client and the server.
Create a new Cargo project and add the following dependencies:
_Cargo.toml_:
<!-- File Cargo.toml -->
```toml
{{#include chat-async/Cargo.toml}}
```
## The required APIs
You are going to need the following functions from `tokio` and
[`tokio_websockets`][2]. Spend a few minutes to familiarize yourself with the
API.
- [StreamExt::next()][3] implemented by `WebSocketStream`: for asynchronously
reading messages from a Websocket Stream.
- [SinkExt::send()][4] implemented by `WebSocketStream`: for asynchronously
sending messages on a Websocket Stream.
- [Lines::next_line()][5]: for asynchronously reading user messages from the
standard input.
- [Sender::subscribe()][6]: for subscribing to a broadcast channel.
## Two binaries
Normally in a Cargo project, you can have only one binary, and one `src/main.rs`
file. In this project, we need two binaries. One for the client, and one for the
server. You could potentially make them two separate Cargo projects, but we are
going to put them in a single Cargo project with two binaries. For this to work,
the client and the server code should go under `src/bin` (see the
[documentation][7]).
Copy the following server and client code into `src/bin/server.rs` and
`src/bin/client.rs`, respectively. Your task is to complete these files as
described below.
_src/bin/server.rs_:
<!-- File src/bin/server.rs -->
```rust,compile_fail
{{#include chat-async/src/bin/server.rs:setup}}
{{#include chat-async/src/bin/server.rs:handle_connection}}
// TODO: For a hint, see the description of the task below.
{{#include chat-async/src/bin/server.rs:main}}
```
_src/bin/client.rs_:
<!-- File src/bin/client.rs -->
```rust,compile_fail
{{#include chat-async/src/bin/client.rs:setup}}
// TODO: For a hint, see the description of the task below.
}
```
## Running the binaries
Run the server with:
```shell
cargo run --bin server
```
and the client with:
```shell
cargo run --bin client
```
## Tasks
- Implement the `handle_connection` function in `src/bin/server.rs`.
- Hint: Use `tokio::select!` for concurrently performing two tasks in a
continuous loop. One task receives messages from the client and broadcasts
them. The other sends messages received by the server to the client.
- Complete the main function in `src/bin/client.rs`.
- Hint: As before, use `tokio::select!` in a continuous loop for concurrently
performing two tasks: (1) reading user messages from standard input and
sending them to the server, and (2) receiving messages from the server, and
displaying them for the user.
- Optional: Once you are done, change the code to broadcast messages to all
clients, but the sender of the message.
[1]: https://docs.rs/tokio/latest/tokio/sync/broadcast/fn.channel.html
[2]: https://docs.rs/tokio-websockets/
[3]: https://docs.rs/futures-util/0.3.28/futures_util/stream/trait.StreamExt.html#method.next
[4]: https://docs.rs/futures-util/0.3.28/futures_util/sink/trait.SinkExt.html#method.send
[5]: https://docs.rs/tokio/latest/tokio/io/struct.Lines.html#method.next_line
[6]: https://docs.rs/tokio/latest/tokio/sync/broadcast/struct.Sender.html#method.subscribe
[7]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries

View File

@ -0,0 +1,10 @@
[package]
name = "chat-async"
version = "0.1.0"
edition = "2021"
[dependencies]
futures-util = { version = "0.3.30", features = ["sink"] }
http = "1.1.0"
tokio = { version = "1.37.0", features = ["full"] }
tokio-websockets = { version = "0.8.2", features = ["client", "fastrand", "server", "sha1_smol"] }

View File

@ -0,0 +1,58 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ANCHOR: solution
// ANCHOR: setup
use futures_util::stream::StreamExt;
use futures_util::SinkExt;
use http::Uri;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio_websockets::{ClientBuilder, Message};
#[tokio::main]
async fn main() -> Result<(), tokio_websockets::Error> {
let (mut ws_stream, _) =
ClientBuilder::from_uri(Uri::from_static("ws://127.0.0.1:2000"))
.connect()
.await?;
let stdin = tokio::io::stdin();
let mut stdin = BufReader::new(stdin).lines();
// ANCHOR_END: setup
// Continuous loop for concurrently sending and receiving messages.
loop {
tokio::select! {
incoming = ws_stream.next() => {
match incoming {
Some(Ok(msg)) => {
if let Some(text) = msg.as_text() {
println!("From server: {}", text);
}
},
Some(Err(err)) => return Err(err.into()),
None => return Ok(()),
}
}
res = stdin.next_line() => {
match res {
Ok(None) => return Ok(()),
Ok(Some(line)) => ws_stream.send(Message::text(line.to_string())).await?,
Err(err) => return Err(err.into()),
}
}
}
}
}

View File

@ -0,0 +1,83 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ANCHOR: solution
// ANCHOR: setup
use futures_util::sink::SinkExt;
use futures_util::stream::StreamExt;
use std::error::Error;
use std::net::SocketAddr;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast::{channel, Sender};
use tokio_websockets::{Message, ServerBuilder, WebSocketStream};
// ANCHOR_END: setup
// ANCHOR: handle_connection
async fn handle_connection(
addr: SocketAddr,
mut ws_stream: WebSocketStream<TcpStream>,
bcast_tx: Sender<String>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
// ANCHOR_END: handle_connection
ws_stream
.send(Message::text("Welcome to chat! Type a message".to_string()))
.await?;
let mut bcast_rx = bcast_tx.subscribe();
// A continuous loop for concurrently performing two tasks: (1) receiving
// messages from `ws_stream` and broadcasting them, and (2) receiving
// messages on `bcast_rx` and sending them to the client.
loop {
tokio::select! {
incoming = ws_stream.next() => {
match incoming {
Some(Ok(msg)) => {
if let Some(text) = msg.as_text() {
println!("From client {addr:?} {text:?}");
bcast_tx.send(text.into())?;
}
}
Some(Err(err)) => return Err(err.into()),
None => return Ok(()),
}
}
msg = bcast_rx.recv() => {
ws_stream.send(Message::text(msg?)).await?;
}
}
}
// ANCHOR: main
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let (bcast_tx, _) = channel(16);
let listener = TcpListener::bind("127.0.0.1:2000").await?;
println!("listening on port 2000");
loop {
let (socket, addr) = listener.accept().await?;
println!("New connection from {addr:?}");
let bcast_tx = bcast_tx.clone();
tokio::spawn(async move {
// Wrap the raw TCP stream into a websocket.
let ws_stream = ServerBuilder::new().accept(socket).await?;
handle_connection(addr, ws_stream, bcast_tx).await
});
}
}
// ANCHOR_END: main

View File

@ -0,0 +1,61 @@
---
minutes: 20
---
# Dining Philosophers --- Async
See [dining philosophers](dining-philosophers.md) for a description of the
problem.
As before, you will need a local
[Cargo installation](../../cargo/running-locally.md) for this exercise. Copy the
code below to a file called `src/main.rs`, fill out the blanks, and test that
`cargo run` does not deadlock:
<!-- File src/main.rs -->
```rust,compile_fail
{{#include dining-philosophers.rs:Philosopher}}
// left_fork: ...
// right_fork: ...
// thoughts: ...
}
{{#include dining-philosophers.rs:Philosopher-think}}
{{#include dining-philosophers.rs:Philosopher-eat}}
{{#include dining-philosophers.rs:Philosopher-eat-body}}
{{#include dining-philosophers.rs:Philosopher-eat-end}}
// Create forks
// Create philosophers
// Make them think and eat
// Output their thoughts
}
```
Since this time you are using Async Rust, you'll need a `tokio` dependency. You
can use the following `Cargo.toml`:
<!-- File Cargo.toml -->
```toml
[package]
name = "dining-philosophers-async-dine"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.26.0", features = ["sync", "time", "macros", "rt-multi-thread"] }
```
Also note that this time you have to use the `Mutex` and the `mpsc` module from
the `tokio` crate.
<details>
- Can you make your implementation single-threaded?
</details>

View File

@ -0,0 +1,119 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ANCHOR: solution
// ANCHOR: Philosopher
use std::sync::Arc;
use tokio::sync::mpsc::{self, Sender};
use tokio::sync::Mutex;
use tokio::time;
struct Fork;
struct Philosopher {
name: String,
// ANCHOR_END: Philosopher
left_fork: Arc<Mutex<Fork>>,
right_fork: Arc<Mutex<Fork>>,
thoughts: Sender<String>,
}
// ANCHOR: Philosopher-think
impl Philosopher {
async fn think(&self) {
self.thoughts
.send(format!("Eureka! {} has a new idea!", &self.name))
.await
.unwrap();
}
// ANCHOR_END: Philosopher-think
// ANCHOR: Philosopher-eat
async fn eat(&self) {
// Keep trying until we have both forks
// ANCHOR_END: Philosopher-eat
let (_left_fork, _right_fork) = loop {
// Pick up forks...
let left_fork = self.left_fork.try_lock();
let right_fork = self.right_fork.try_lock();
let Ok(left_fork) = left_fork else {
// If we didn't get the left fork, drop the right fork if we
// have it and let other tasks make progress.
drop(right_fork);
time::sleep(time::Duration::from_millis(1)).await;
continue;
};
let Ok(right_fork) = right_fork else {
// If we didn't get the right fork, drop the left fork and let
// other tasks make progress.
drop(left_fork);
time::sleep(time::Duration::from_millis(1)).await;
continue;
};
break (left_fork, right_fork);
};
// ANCHOR: Philosopher-eat-body
println!("{} is eating...", &self.name);
time::sleep(time::Duration::from_millis(5)).await;
// ANCHOR_END: Philosopher-eat-body
// The locks are dropped here
// ANCHOR: Philosopher-eat-end
}
}
static PHILOSOPHERS: &[&str] =
&["Socrates", "Hypatia", "Plato", "Aristotle", "Pythagoras"];
#[tokio::main]
async fn main() {
// ANCHOR_END: Philosopher-eat-end
// Create forks
let mut forks = vec![];
(0..PHILOSOPHERS.len()).for_each(|_| forks.push(Arc::new(Mutex::new(Fork))));
// Create philosophers
let (philosophers, mut rx) = {
let mut philosophers = vec![];
let (tx, rx) = mpsc::channel(10);
for (i, name) in PHILOSOPHERS.iter().enumerate() {
let left_fork = Arc::clone(&forks[i]);
let right_fork = Arc::clone(&forks[(i + 1) % PHILOSOPHERS.len()]);
philosophers.push(Philosopher {
name: name.to_string(),
left_fork,
right_fork,
thoughts: tx.clone(),
});
}
(philosophers, rx)
// tx is dropped here, so we don't need to explicitly drop it later
};
// Make them think and eat
for phil in philosophers {
tokio::spawn(async move {
for _ in 0..100 {
phil.think().await;
phil.eat().await;
}
});
}
// Output their thoughts
while let Some(thought) = rx.recv().await {
println!("Here is a thought: {thought}");
}
}

View File

@ -0,0 +1,25 @@
---
minutes: 20
---
# Solutions
## Dining Philosophers --- Async
```rust,compile_fail
{{#include dining-philosophers.rs:solution}}
```
## Broadcast Chat Application
_src/bin/server.rs_:
```rust,compile_fail
{{#include chat-async/src/bin/server.rs:solution}}
```
_src/bin/client.rs_:
```rust,compile_fail
{{#include chat-async/src/bin/client.rs:solution}}
```