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

Safer word lock #108

Merged
merged 7 commits into from
Dec 3, 2018
Merged

Safer word lock #108

merged 7 commits into from
Dec 3, 2018

Conversation

faern
Copy link
Collaborator

@faern faern commented Dec 2, 2018

I tried to reason a bit about the invariants of the unsafe code in WordLock. I think we can get away with them not being unsafe with some minor adjustments. Mostly this PR removes unsafe keywords from methods and adds unsafe blocks within them. But there is one more difference that I'm hoping should make this safe to call from the outside:

fn unlock(&self) {
    let state = self.state.fetch_and(!LOCKED_BIT, Ordering::Release);
    ...
}

Switching from fetch_sub to fetch_and is intended to make the method not corrupt self.state if called on an already unlocked WordLock. Previously multiple calls would continue to decrement the integer by one, locking and unlocking it as well as corrupting the part of it that is used as a pointer. By using the bit and operation this should just zero out the LOCKED_BIT without touching the rest. The test suite passes, I'm not sure if I have missed something else that makes this a bad change.

If you don't like the trait on usize providing the convenience methods we can skip them. They are unrelated to making the methods safe, other than it might be harder to screw code up if the bit manipulation is extracted.

@Amanieu
Copy link
Owner

Amanieu commented Dec 2, 2018

There is a good reason for using fetch_sub instead of fetch_and: it results in better code generation on x86 since it allows the use of the xadd instruction. fetch_and on the other hand requires a compare-exchange loop.

Code

core/src/word_lock.rs Outdated Show resolved Hide resolved
fn is_locked(&self) -> bool;
fn is_queue_locked(&self) -> bool;
fn queue_head(&self) -> *const ThreadData;
fn set_queue_head(&self, thread_data: *const ThreadData) -> Self;
Copy link
Owner

Choose a reason for hiding this comment

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

This doesn't modify self, so maybe with_queue_head is a better name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That makes sense. Will also make every method consume self since it's just a Copy type anyway.

@faern
Copy link
Collaborator Author

faern commented Dec 2, 2018

Ah, crap. I suspected that was the reason. So I will make lock unsafe again and keep lock_slow safe. Still way less code under unsafe.

@faern faern force-pushed the safer-word-lock branch 2 times, most recently from c4f0ee2 to f164c81 Compare December 2, 2018 19:38
}

// If ThreadData is expensive to construct, then we want to use a cached
// version in thread-local storage if possible.
if !cfg!(windows) && !cfg!(all(feature = "nightly", target_os = "linux")) {
thread_local!(static THREAD_DATA: ThreadData = ThreadData::new());
if let Some(tls) = try_get_tls(&THREAD_DATA) {
return &*tls;
if let Some(result) = try_with_tls(&THREAD_DATA, &mut f) {
Copy link
Owner

Choose a reason for hiding this comment

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

OK, so this is a bit subtle, but I would prefer if f was only called from one place and you simply extracted a reference from the thread-local storage. The reason for this is that if f is used in 2 different places, it will effectively be inlined twice into the function, which doubles the code size. This will require a bit of unsafe code to juggle the lifetimes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, yeah. Makes sense. I did this now, but without much unsafe. Instead of juggling too many pointers I just stored the reference or owned value in a Cow<ThreadData>

@faern faern force-pushed the safer-word-lock branch 2 times, most recently from ff6ae74 to 69372b7 Compare December 3, 2018 00:13
if thread_data_ptr.is_null() {
// Otherwise just create a ThreadData on the stack
thread_data_storage = Some(ThreadData::new());
thread_data_ptr = thread_data_storage.as_ref().unwrap();
Copy link
Owner

Choose a reason for hiding this comment

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

You can use Option::get_or_insert_with instead of this if block.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't see how that would work here. thread_data_storage is always None when reaching this if block. So the closure to get_or_insert_with would always run and I would still need to get the address out for my pointer variable later. My initial declaration of thread_data_storage was a bit off, but I moved it now to better show how it's used.

Copy link
Owner

Choose a reason for hiding this comment

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

thread_data_ptr = thread_data_storage.get_or_insert_with(ThreadData::new);

Since get_or_insert_with returns a reference to the inner value in the option, you can avoid calling as_ref and unwrap.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

🤦‍♂️ I completely missed it returns a reference. Of course it does.

@faern faern force-pushed the safer-word-lock branch 3 times, most recently from a5d45ec to afa5b28 Compare December 3, 2018 12:11
@Amanieu Amanieu merged commit ec0b68e into Amanieu:master Dec 3, 2018
@Amanieu
Copy link
Owner

Amanieu commented Dec 3, 2018

Thanks!

@faern faern deleted the safer-word-lock branch December 3, 2018 16:33
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