@@ -1,39 +1,39 @@
|
||||
# Locks, `Send` and `Arc`
|
||||
|
||||
The patching strategy you just implemented has a major drawback: it's racy.
|
||||
The patching strategy you just implemented has a major drawback: it's racy.\
|
||||
If two clients send patches for the same ticket roughly at same time, the server will apply them in an arbitrary order.
|
||||
Whoever enqueues their patch last will overwrite the changes made by the other client.
|
||||
|
||||
## Version numbers
|
||||
|
||||
We could try to fix this by using a **version number**.
|
||||
Each ticket gets assigned a version number upon creation, set to `0`.
|
||||
Whenever a client sends a patch, they must include the current version number of the ticket alongside the
|
||||
We could try to fix this by using a **version number**.\
|
||||
Each ticket gets assigned a version number upon creation, set to `0`.\
|
||||
Whenever a client sends a patch, they must include the current version number of the ticket alongside the
|
||||
desired changes. The server will only apply the patch if the version number matches the one it has stored.
|
||||
|
||||
In the scenario described above, the server would reject the second patch, because the version number would
|
||||
In the scenario described above, the server would reject the second patch, because the version number would
|
||||
have been incremented by the first patch and thus wouldn't match the one sent by the second client.
|
||||
|
||||
This approach is fairly common in distributed systems (e.g. when client and servers don't share memory),
|
||||
and it is known as **optimistic concurrency control**.
|
||||
and it is known as **optimistic concurrency control**.\
|
||||
The idea is that most of the time, conflicts won't happen, so we can optimize for the common case.
|
||||
You know enough about Rust by now to implement this strategy on your own as a bonus exercise, if you want to.
|
||||
|
||||
## Locking
|
||||
|
||||
We can also fix the race condition by introducing a **lock**.
|
||||
We can also fix the race condition by introducing a **lock**.\
|
||||
Whenever a client wants to update a ticket, they must first acquire a lock on it. While the lock is active,
|
||||
no other client can modify the ticket.
|
||||
|
||||
Rust's standard library provides two different locking primitives: `Mutex<T>` and `RwLock<T>`.
|
||||
Rust's standard library provides two different locking primitives: `Mutex<T>` and `RwLock<T>`.\
|
||||
Let's start with `Mutex<T>`. It stands for **mut**ual **ex**clusion, and it's the simplest kind of lock:
|
||||
it allows only one thread to access the data, no matter if it's for reading or writing.
|
||||
|
||||
`Mutex<T>` wraps the data it protects, and it's therefore generic over the type of the data.
|
||||
You can't access the data directly: the type system forces you to acquire a lock first using either `Mutex::lock` or
|
||||
`Mutex<T>` wraps the data it protects, and it's therefore generic over the type of the data.\
|
||||
You can't access the data directly: the type system forces you to acquire a lock first using either `Mutex::lock` or
|
||||
`Mutex::try_lock`. The former blocks until the lock is acquired, the latter returns immediately with an error if the lock
|
||||
can't be acquired.
|
||||
Both methods return a guard object that dereferences to the data, allowing you to modify it. The lock is released when
|
||||
can't be acquired.\
|
||||
Both methods return a guard object that dereferences to the data, allowing you to modify it. The lock is released when
|
||||
the guard is dropped.
|
||||
|
||||
```rust
|
||||
@@ -57,10 +57,10 @@ drop(guard)
|
||||
|
||||
## Locking granularity
|
||||
|
||||
What should our `Mutex` wrap?
|
||||
The simplest option would be the wrap the entire `TicketStore` in a single `Mutex`.
|
||||
What should our `Mutex` wrap?\
|
||||
The simplest option would be the wrap the entire `TicketStore` in a single `Mutex`.\
|
||||
This would work, but it would severely limit the system's performance: you wouldn't be able to read tickets in parallel,
|
||||
because every read would have to wait for the lock to be released.
|
||||
because every read would have to wait for the lock to be released.\
|
||||
This is known as **coarse-grained locking**.
|
||||
|
||||
It would be better to use **fine-grained locking**, where each ticket is protected by its own lock.
|
||||
@@ -73,17 +73,17 @@ struct TicketStore {
|
||||
}
|
||||
```
|
||||
|
||||
This approach is more efficient, but it has a downside: `TicketStore` has to become **aware** of the multithreaded
|
||||
nature of the system; up until now, `TicketStore` has been blissfully ignored the existence of threads.
|
||||
This approach is more efficient, but it has a downside: `TicketStore` has to become **aware** of the multithreaded
|
||||
nature of the system; up until now, `TicketStore` has been blissfully ignored the existence of threads.\
|
||||
Let's go for it anyway.
|
||||
|
||||
## Who holds the lock?
|
||||
|
||||
For the whole scheme to work, the lock must be passed to the client that wants to modify the ticket.
|
||||
For the whole scheme to work, the lock must be passed to the client that wants to modify the ticket.\
|
||||
The client can then directly modify the ticket (as if they had a `&mut Ticket`) and release the lock when they're done.
|
||||
|
||||
This is a bit tricky.
|
||||
We can't send a `Mutex<Ticket>` over a channel, because `Mutex` is not `Clone` and
|
||||
This is a bit tricky.\
|
||||
We can't send a `Mutex<Ticket>` over a channel, because `Mutex` is not `Clone` and
|
||||
we can't move it out of the `TicketStore`. Could we send the `MutexGuard` instead?
|
||||
|
||||
Let's test the idea with a small example:
|
||||
@@ -131,22 +131,22 @@ note: required because it's used within this closure
|
||||
|
||||
## `Send`
|
||||
|
||||
`Send` is a marker trait that indicates that a type can be safely transferred from one thread to another.
|
||||
`Send` is a marker trait that indicates that a type can be safely transferred from one thread to another.\
|
||||
`Send` is also an auto-trait, just like `Sized`; it's automatically implemented (or not implemented) for your type
|
||||
by the compiler, based on its definition.
|
||||
You can also implement `Send` manually for your types, but it requires `unsafe` since you have to guarantee that the
|
||||
by the compiler, based on its definition.\
|
||||
You can also implement `Send` manually for your types, but it requires `unsafe` since you have to guarantee that the
|
||||
type is indeed safe to send between threads for reasons that the compiler can't automatically verify.
|
||||
|
||||
### Channel requirements
|
||||
|
||||
`Sender<T>`, `SyncSender<T>` and `Receiver<T>` are `Send` if and only if `T` is `Send`.
|
||||
`Sender<T>`, `SyncSender<T>` and `Receiver<T>` are `Send` if and only if `T` is `Send`.\
|
||||
That's because they are used to send values between threads, and if the value itself is not `Send`, it would be
|
||||
unsafe to send it between threads.
|
||||
|
||||
### `MutexGuard`
|
||||
|
||||
`MutexGuard` is not `Send` because the underlying operating system primitives that `Mutex` uses to implement
|
||||
the lock require (on some platforms) that the lock must be released by the same thread that acquired it.
|
||||
the lock require (on some platforms) that the lock must be released by the same thread that acquired it.\
|
||||
If we were to send a `MutexGuard` to another thread, the lock would be released by a different thread, which would
|
||||
lead to undefined behavior.
|
||||
|
||||
@@ -154,13 +154,13 @@ lead to undefined behavior.
|
||||
|
||||
Summing it up:
|
||||
|
||||
- We can't send a `MutexGuard` over a channel. So we can't lock on the server-side and then modify the ticket on the
|
||||
- We can't send a `MutexGuard` over a channel. So we can't lock on the server-side and then modify the ticket on the
|
||||
client-side.
|
||||
- We can send a `Mutex` over a channel because it's `Send` as long as the data it protects is `Send`, which is the
|
||||
case for `Ticket`.
|
||||
At the same time, we can't move the `Mutex` out of the `TicketStore` nor clone it.
|
||||
- We can send a `Mutex` over a channel because it's `Send` as long as the data it protects is `Send`, which is the
|
||||
case for `Ticket`.
|
||||
At the same time, we can't move the `Mutex` out of the `TicketStore` nor clone it.
|
||||
|
||||
How can we solve this conundrum?
|
||||
How can we solve this conundrum?\
|
||||
We need to look at the problem from a different angle.
|
||||
To lock a `Mutex`, we don't need an owned value. A shared reference is enough, since `Mutex` uses internal mutability:
|
||||
|
||||
@@ -173,15 +173,15 @@ impl<T> Mutex<T> {
|
||||
}
|
||||
```
|
||||
|
||||
It is therefore enough to send a shared reference to the client.
|
||||
We can't do that directly, though, because the reference would have to be `'static` and that's not the case.
|
||||
It is therefore enough to send a shared reference to the client.\
|
||||
We can't do that directly, though, because the reference would have to be `'static` and that's not the case.\
|
||||
In a way, we need an "owned shared reference". It turns out that Rust has a type that fits the bill: `Arc`.
|
||||
|
||||
## `Arc` to the rescue
|
||||
|
||||
`Arc` stands for **atomic reference counting**.
|
||||
`Arc` stands for **atomic reference counting**.\
|
||||
`Arc` wraps around a value and keeps track of how many references to the value exist.
|
||||
When the last reference is dropped, the value is deallocated.
|
||||
When the last reference is dropped, the value is deallocated.\
|
||||
The value wrapped in an `Arc` is immutable: you can only get shared references to it.
|
||||
|
||||
```rust
|
||||
@@ -196,9 +196,9 @@ let data_ref: &u32 = &data;
|
||||
```
|
||||
|
||||
If you're having a déjà vu moment, you're right: `Arc` sounds very similar to `Rc`, the reference-counted pointer we
|
||||
introduced when talking about interior mutability. The difference is thread-safety: `Rc` is not `Send`, while `Arc` is.
|
||||
It boils down to the way the reference count is implemented: `Rc` uses a "normal" integer, while `Arc` uses an
|
||||
**atomic** integer, which can be safely shared and modified across threads.
|
||||
introduced when talking about interior mutability. The difference is thread-safety: `Rc` is not `Send`, while `Arc` is.
|
||||
It boils down to the way the reference count is implemented: `Rc` uses a "normal" integer, while `Arc` uses an
|
||||
**atomic** integer, which can be safely shared and modified across threads.
|
||||
|
||||
## `Arc<Mutex<T>>`
|
||||
|
||||
@@ -209,9 +209,9 @@ If we pair `Arc` with `Mutex`, we finally get a type that:
|
||||
- `Mutex` is `Send` if `T` is `Send`.
|
||||
- `T` is `Ticket`, which is `Send`.
|
||||
- Can be cloned, because `Arc` is `Clone` no matter what `T` is.
|
||||
Cloning an `Arc` increments the reference count, the data is not copied.
|
||||
Cloning an `Arc` increments the reference count, the data is not copied.
|
||||
- Can be used to modify the data it wraps, because `Arc` lets you get a shared
|
||||
reference to `Mutex<T>` which can in turn be used to acquire a lock.
|
||||
reference to `Mutex<T>` which can in turn be used to acquire a lock.
|
||||
|
||||
We have all the pieces we need to implement the locking strategy for our ticket store.
|
||||
|
||||
@@ -219,4 +219,4 @@ We have all the pieces we need to implement the locking strategy for our ticket
|
||||
|
||||
- We won't be covering the details of atomic operations in this course, but you can find more information
|
||||
[in the `std` documentation](https://doc.rust-lang.org/std/sync/atomic/index.html) as well as in the
|
||||
["Rust atomics and locks" book](https://marabos.nl/atomics/).
|
||||
["Rust atomics and locks" book](https://marabos.nl/atomics/).
|
||||
|
||||
Reference in New Issue
Block a user