Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RwLock::{try_read, try_write} #72

Merged
merged 1 commit into from
Jul 7, 2022

Conversation

jamesbornholt
Copy link
Member

This is a little tricky because std is unclear about whether a thread
can acquire the same read lock multiple times. For read it says:

This function might panic when called if the lock is already held by
the current thread.

So acquiring a second read lock might fail. But for try_read it says:

This function will return the WouldBlock error if the RwLock could not
be acquired because it was already locked exclusively.

suggesting that try_read must succeed the second time (the lock is
not held exclusively).

We resolve this ambiguity by choosing a conservative semantics that
always forbids a thread acquiring the read lock twice. This helps us
catch deadlocks, especially in async programs where a task might
nondeterministically migrate between threads and only deadlock if that
migration didn't happen.

Another difficulty with this change is that causality is pretty hairy
for the read side of a RwLock. In principle, concurrent readers
shouldn't inherit each other's causality, as they don't affect whether
the lock was readable or not. But that's really hard to implement,
especially with try_write in the picture too. So again we choose a
conservative implementation for our vector clocks that just always
inherits causality from all prior lock holders. This is likely too
strong, so we'd explore unnecessary symmetries, but I can't convince
myself of the correctness of a weaker implementation.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

src/sync/rwlock.rs Show resolved Hide resolved
src/sync/rwlock.rs Show resolved Hide resolved
// Check that the only nonzero clock entries are for the threads in the set
check_clock(|i, c| (c > 0) == set.contains(&i));
assert!(!set.contains(&1)); // dummy thread is never in the set
check_clock(|i, c| !set.contains(&i) || (c > 0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear why you weakened this check from an iff to an implication?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to handle race condition described at the top of the test—readers can have a non-zero clock without being in the set.

tests/basic/rwlock.rs Outdated Show resolved Hide resolved
tests/basic/rwlock.rs Outdated Show resolved Hide resolved
/// third that peeks at the value using `try_read.` The `write` must succeed, while each `try_write`
/// may or may not succeed. Thus we expect to see final values:
/// * 0 (`try_read` ran first)
/// * 1 (both `try_writes`s fail),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the reader may see a final value of 1 even if one (or both) try_writes were successful (if the reader went before the try_writes). So this comment is misleading.

One thing you could do is to read the value again after the joins, and then look at all the pairs of values (value_read_by_try_read, value_read_after_joins).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I updated the test to check all the possible pairs of values.

/// easily diagnose potential deadlocks, especially with async tasks that might migrate across
/// threads in real implementations.
#[test]
fn double_try_read() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a variant of this where a thread does a read() and then a try_read()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured this would be the same as if the first try_read succeeds (assuming that a successful try_read and a read are equivalent), but may as well add the explicit test anyway.

This is a little tricky because `std` is unclear about whether a thread
can acquire the same read lock multiple times. For `read` it says:

> This function might panic when called if the lock is already held by
> the current thread.

So acquiring a second read lock _might_ fail. But for `try_read` it says:

> This function will return the WouldBlock error if the RwLock could not
> be acquired because it was already locked exclusively.

suggesting that `try_read` _must_ succeed the second time (the lock is
not held exclusively).

We resolve this ambiguity by choosing a conservative semantics that
always forbids a thread acquiring the read lock twice. This helps us
catch deadlocks, especially in async programs where a task might
nondeterministically migrate between threads and only deadlock if that
migration didn't happen.

Another difficulty with this change is that causality is pretty hairy
for the read side of a `RwLock`. In principle, concurrent readers
shouldn't inherit each other's causality, as they don't affect whether
the lock was readable or not. But that's really hard to implement,
especially with `try_write` in the picture too. So again we choose a
conservative implementation for our vector clocks that just always
inherits causality from all prior lock holders. This is likely too
strong, so we'd explore unnecessary symmetries, but I can't convince
myself of the correctness of a weaker implementation.
Comment on lines -339 to +348
let _ = rx.recv().unwrap();
rx.recv().unwrap();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a Clippy 1.62 thing.

@jorajeev jorajeev merged commit 3a007d6 into awslabs:main Jul 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants