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

sync: Implement map methods of parking_lot fame #2445

Merged
merged 1 commit into from Aug 26, 2020

Conversation

udoprog
Copy link
Contributor

@udoprog udoprog commented Apr 25, 2020

I've introduced MappedRwLockReadGuard and MappedRwLockWriteGuard, which are produced by the relevant map and try_map methods.

Generally, this mimics the way MappedRwLock*Guards are implemented in parking_lot. By storing a raw pointer in the guards themselves referencing the mapped data and maintaining type invariants through PhantomData. I didn't try to think too much about this, so if someone has objections I'd love to hear them.

I've also dropped the internal use of ReleasingPermit, since it made the guards unecessarily large. The number of permits that need to be released are already known by the guards themselves, and is instead governed directly in the relevant Drop impls. This has the benefit of making the guards as small as possible, for the non-mapped variants this means a single reference is enough.

fmt::Debug impls have been adjusted to behave exactly like the delegating impls in parking_lot. fmt::Display impls have been added for all guard types which behave the same. This does change the format of debug impls, for which I'm not sure if we provide any guarantees.

Closes #2442 since it duplicates some of those efforts. If we don't want map methods I can resurrect only the relevant changes.

@udoprog udoprog changed the title sync: Implement map methods by parking_lot fame sync: Implement map methods of parking_lot fame Apr 26, 2020
@udoprog udoprog force-pushed the rwlock-map branch 2 times, most recently from ae9d6ef to dd7198b Compare April 26, 2020 12:33
@Darksonn Darksonn added A-tokio Area: The main tokio crate C-enhancement Category: A PR with an enhancement or bugfix. M-sync Module: tokio/sync S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 28, 2020
@Darksonn Darksonn requested a review from hawkw April 29, 2020 10:14
@hawkw
Copy link
Member

hawkw commented Apr 30, 2020

May also want to note that this closes #2471 as well :)

where
F: FnOnce(&T) -> &U,
{
let data = f(unsafe { &*self.lock.c.get() });
Copy link
Member

Choose a reason for hiding this comment

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

Is it necessary for this to be unsafe? Can we just dereference the guard normally here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

@udoprog I believe you can probably remove the usages of unsafe from several other places too using the same technique. You still have several map and try_map functions that use unsafe in the same way you were before.

tokio/src/sync/rwlock.rs Show resolved Hide resolved
tokio/src/sync/rwlock.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@sunjay sunjay left a comment

Choose a reason for hiding this comment

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

Hi @udoprog! I am the author of issue #2471 and PR #2472. Thank you so much for your work on this PR. Using your work as a reference made creating my PR much easier!

As a result of doing that PR, I ended up having a very close look at this one and noticed a few things that should probably be addressed. I hope you don't mind that I left a few comments.

Thanks again! Looking forward to this landing. :)

/// # }
/// ```
#[inline]
pub fn map<U, F>(self, f: F) -> MappedRwLockReadGuard<'a, U>
Copy link
Contributor

Choose a reason for hiding this comment

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

The map/try_map methods should probably be associated functions (using e.g. this: Self) instead of methods that take self. This is important to avoid conflicts with methods on the dereferenced type T. The map methods on parking_lot are associated functions too.

(In fact, your documentation on this method even says "This is an associated function".)

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, are you planning on adding map and try_map associated functions on MappedRwLockReadGuard and MappedRwLockWriteGuard? Without that it will only be possible to map the data once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the pointer. My head just refactored the methods into self without realizing why.

Also, are you planning on adding map and try_map associated functions on MappedRwLockReadGuard and MappedRwLockWriteGuard? Without that it will only be possible to map the data once.

Not at the moment since I personally have no use for it. Feel free to add it in a follow up to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was proof-reading documentation and decided to add associated methods for the mapped variants. Please look them over. Thanks.

// NB: These impls need to be explicit since we're storing a raw pointer.
// Safety: Stores a raw pointer to `T`, so if `T` is `Sync`, so is the lock
// guard over `T`.
unsafe impl<'a, T> Send for MappedRwLockReadGuard<'a, T> where T: Sync {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these be implementing Sync, not Send? I don't know for sure, but it's suspicious given the code around it and the comment above it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Modified the comment. The impl is indeed supposed to be Send if we want the guard to be sent across .await for Send futures. See if it makes sense to you now.

@sunjay
Copy link
Contributor

sunjay commented May 12, 2020

@udoprog I managed to merge MappedMutexGuard into MutexGuard in #2472, do you think the same could be possible here for the new guards you introduced? Link to the relevant commits

@udoprog
Copy link
Contributor Author

udoprog commented May 12, 2020

@sunjay Yeah, for sure.

I just want to make sure that if we want to support upgradeable/downgradeable locks at some point we're not subject to soundness issues as commented here and in the documentation for the guard. I haven't wrapped my head around that issue yet though, so just mimicking the API for now.

@udoprog
Copy link
Contributor Author

udoprog commented May 12, 2020

Eh. So the downgrade API for Mapped*Guard is being deprecated. Maybe not something we should mimic then. I need to look closer at it. 🤔

It is being kept though for RwLockWriteGuard (but not the mapped version) which seems to be an important distinction.

@udoprog
Copy link
Contributor Author

udoprog commented Jul 9, 2020

Bumped to latest changes in Tokio.

I still need this, so would like to somehow move it forward. If someone wants to see how it's used: https://github.com/udoprog/OxidizeBot/search?q=try_map&unscoped_q=try_map

@hawkw
Copy link
Member

hawkw commented Jul 10, 2020

With regards to supporting upgrade/downgrade for guards: I'm not sure how much sense this would make with the current implementation of the RwLock and the way fairness is currently ensured.

Upgrading a RwLockReadGuard would mean enqueuing it at the end of the queue to acquire all of the remaining semaphore permits (and thus the write lock) while still holding a single permit for the read lock. This means that if any attempts to acquire the write lock are already in the queue, they will never be able to acquire the lock, since the queued upgrade is currently holding one of the permits. Because permits are only ever assigned to the head of the queue, this means that attempting to upgrade a read lock while any other task is waiting for a write lock would result in a deadlock. We could avoid this issue by having an upgrade method that releases any currently held permits, but this is then morally equivalent to just dropping the read guard and trying to acquire a write guard...meaning a separate upgrade method may not be strictly necessary.

Given that, IMO, I think I'd prefer to merge the change that presents a smaller API surface for now. If we end up changing our minds about this, we'll have the chance to break the API in 0.3, so we could change the map functions to return a different guard type then.

It would be nice to see if @carllerche agrees, though!

@udoprog
Copy link
Contributor Author

udoprog commented Jul 10, 2020

I'm fine with either at this point. Removed the other guard types for now.

Copy link
Member

@hawkw hawkw left a comment

Choose a reason for hiding this comment

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

Looks good to me --- sorry for the delay on the review.

tokio/src/sync/rwlock.rs Outdated Show resolved Hide resolved
tokio/src/sync/rwlock.rs Outdated Show resolved Hide resolved
tokio/src/sync/rwlock.rs Outdated Show resolved Hide resolved
tokio/src/sync/rwlock.rs Outdated Show resolved Hide resolved
@hawkw hawkw requested a review from carllerche July 10, 2020 17:30
@udoprog udoprog force-pushed the rwlock-map branch 4 times, most recently from 51bdc6f to 3812179 Compare July 24, 2020 04:37
@hawkw
Copy link
Member

hawkw commented Jul 24, 2020

note that if PR #2548 merges before this (which I suspect it will), we'll need to rebase this branch and update it, since #2548 adds additional uses of ReleasingPermit that will need to be changed.

@udoprog
Copy link
Contributor Author

udoprog commented Jul 24, 2020

Yeah. I'm pretty used to rebasing now.

Generally, this mimics the way `MappedRwLock*Guard`s are implemented in
`parking_lot`. By storing a raw pointer in the guards themselves
referencing the mapped data and maintaining type invariants through
`PhantomData`. I didn't try to think too much about this, so if someone
has objections I'd love to hear them.

I've also dropped the internal use of `ReleasingPermit`, since it made
the guards unecessarily large. The number of permits that need to be
released are already known by the guards themselves, and is instead
governed directly in the relevant `Drop` impls.  This has the benefit of
making the guards as small as possible, for the non-mapped variants this
means a single reference is enough.

`fmt::Debug` impls have been adjusted to behave exactly like the
delegating impls in `parking_lot`. `fmt::Display` impls have been added
for all guard types which behave the same. This does change the format
of debug impls, for which I'm not sure if we provide any guarantees.
@udoprog
Copy link
Contributor Author

udoprog commented Aug 16, 2020

Rebased.

@carllerche I think everyone is waiting for an OK from someone because of the API addition, and I think that's you? But not sure. Could you have a look?

@Darksonn Darksonn removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 26, 2020
@Darksonn Darksonn merged commit 2e7e42b into tokio-rs:master Aug 26, 2020
@udoprog udoprog deleted the rwlock-map branch August 26, 2020 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate C-enhancement Category: A PR with an enhancement or bugfix. M-sync Module: tokio/sync
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants