-
Notifications
You must be signed in to change notification settings - Fork 766
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
Add PyMutex wrappers #4523
base: main
Are you sure you want to change the base?
Add PyMutex wrappers #4523
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks very much for implementing this! I have a bunch of initial thoughts, mostly we should understand what the invariant around moving the FFI mutex is before we worry too much about the fine details. (If it is not ok to move it when unlocked, we will need to do more complicated APIs which make use of pinning, I fear...)
src/types/mutex.rs
Outdated
} | ||
|
||
/// Create a new mutex in an unlocked state ready for use. | ||
pub fn new(value: T) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be const
pub fn new(value: T) -> Self { | |
pub const fn new(value: T) -> Self { |
src/types/mutex.rs
Outdated
/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex) exposing an RAII interface. | ||
#[derive(Debug)] | ||
pub struct PyMutex<T> { | ||
_mutex: crate::ffi::PyMutex, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This design would permit moving the mutex while it's unlocked. As far as the C API docs state, they just say the mutex must never be moved. @colesbury is it permissible to move the mutex while it's unlocked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's the same restrictions as pthread_mutex_t
. I think that's fine if Rust allows it for std::sync::mutex
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read the C api documentation as "once you create/use this, you can never move it again", which is why I pushed for the PhantomPinned
. If it really means "Instances of PyMutex should not be copied or moved while it is locked.", we can remove that.
Also, while I appreciate your clarification here, would you mind adding it to https://docs.python.org/3.14/c-api/init.html#c.PyMutex ? I would feel a lot better if I can point to official documentation rather than a comment on a PR.
src/types/mutex.rs
Outdated
use std::ops::Deref; | ||
|
||
/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex) exposing an RAII interface. | ||
#[derive(Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a derived Debug
implementation here is unsound as it could allow reading while another thread is writing. We should check what the debug implementation for std::sync::Mutex
prints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Std's Mutex' Debug impl uses try_lock
, which PyMutex doesn't have. We can have a Debug impl, it just can't print any of its contents.
src/types/mutex.rs
Outdated
@@ -0,0 +1,86 @@ | |||
use std::ops::Deref; | |||
|
|||
/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex) exposing an RAII interface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we should probably advise that the advantage of this is that it automatically detaches from python thread states if necessary.
It wouldn't be obvious to me, if I knew nothing, how or why this mutex works. The design is very unlike the mutex in std or parking_lot. We should come up with a couple of good use cases and design the api accordingly. |
This would be easy, we'd just need to change PyMutex::lock to take |
Right, yes. I think however that probably we should change That way we still cannot move the mutex while locked, and we also make the API more like std or |
Pin would only be necessary if we'd want to enforce that the mutex is never moved once used. Anyway, this implementation is not an interior mutability primitive. I think most users will expect that. |
This mutex has the nice feature that if the thread trying to acquire the mutex blocks and holds the GIL, it will release the GIL. The use case is if you need to lock something while still executing arbitrary python code that might trigger releasing or acquiring the GIL. It's a good point we should wait to add this until we have more of a need for it. Let's see if it we have a need for it in the internal uses of |
If we want this to be "like std's Mutex, but it releases the gil while it blocks" that's fine with me, but this PR needs a lot of work in that case. |
@colesbury I have a question about the behavior - if we lock it twice on the same thread, say we do PyMutex mutex = {0};
PyMutex_Lock(&mutex);
PyMutex_Lock(&mutex);
PyMutex_Unlock(&mutex);
PyMutex_Unlock(&mutex); will this deadlock? I'm guessing yes, because it doesn't say recursive locking is allowed, and it's what I would expect from |
Yes, it deadlocks
…On Wed, Sep 4, 2024 at 8:16 PM Bruno Kolenbrander ***@***.***> wrote:
@colesbury <https://github.com/colesbury> I have a question about the
behavior - if we lock it twice on the same thread, say we do
PyMutex mutex = {0};PyMutex_Lock(&mutex);PyMutex_Lock(&mutex);PyMutex_Unlock(&mutex);PyMutex_Unlock(&mutex);
will this deadlock? I'm guessing yes, because it doesn't say recursive
locking is allowed, and it's what I would expect from pthread-like api.
I'm asking because if this does not deadlock it is problematic for us (it
lets you get multiple MutexGuards and thus aliasing mutable references).
—
Reply to this email directly, view it on GitHub
<#4523 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFAD6S4AMRVLMPQ4HBSBF3ZU6PG3AVCNFSM6AAAAABNS3KY3KVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGMZQGM3DANJSHE>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
src/types/mutex.rs
Outdated
#[derive(Debug)] | ||
pub struct PyMutex<T> { | ||
_mutex: crate::ffi::PyMutex, | ||
data: T, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be UnsafeCell<T>
We could probably write a test that it deadlocks by spawning a thread and allowing that thread to deadlock, and can assert that thread doesn't run to completion after e.g. 1 second. |
src/types/mutex.rs
Outdated
pub struct PyMutex<T> { | ||
_mutex: crate::ffi::PyMutex, | ||
data: T, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub struct PyMutex<T> { | |
_mutex: crate::ffi::PyMutex, | |
data: T, | |
} | |
pub struct PyMutex<T> { | |
_mutex: UnsafeCell<crate::ffi::PyMutex>, | |
data: UnsafeCell<T>, | |
} |
- both cells are needed; one for lock / unlock, and one for data to be readable from interior mutability.
src/types/mutex.rs
Outdated
_mutex: &'a mut crate::ffi::PyMutex, | ||
data: &'a T, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_mutex: &'a mut crate::ffi::PyMutex, | |
data: &'a T, | |
mutex: &'a PyMutex<T>, |
... and then can read from inside the mutex's UnsafeCell
for the Deref
and DerefMut
implementations.
41e87b3
to
1229634
Compare
I'm having trouble writing this test because I run into errors like Am I missing something? |
Probably missing |
This is incorrect, it must be |
Thanks for the hint! With the last push I can write a test that deadlocks:
but I have no idea how to write a rust test that safely terminates a deadlocked thread after a timeout. Can you point me to an example I can look at somewhere? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should get consensus on what we want this api to look like; do we want it to mirror std's mutex? (the reviews so far assume as much).
The Send and Sync (un)implementations are quite tricky to get right. Even std got this wrong initially. I think the implementations of this Mutex and MutexGuard should be the same as std's.
pyo3-ffi/src/cpython/lock.rs
Outdated
@@ -8,6 +8,15 @@ pub struct PyMutex { | |||
pub(crate) _pin: PhantomPinned, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This not part of the PR but since colesbury's clarification I believe this is unnecessary, so it can be removed.
src/types/mutex.rs
Outdated
use std::ops::Deref; | ||
|
||
/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex) exposing an RAII interface. | ||
#[derive(Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Std's Mutex' Debug impl uses try_lock
, which PyMutex doesn't have. We can have a Debug impl, it just can't print any of its contents.
src/types/mutex.rs
Outdated
/// RAII guard to handle releasing a PyMutex lock. | ||
#[derive(Debug)] | ||
pub struct PyMutexGuard<'a, T> { | ||
mutex: &'a mut PyMutex<T>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mutex: &'a mut PyMutex<T>, | |
mutex: &'a PyMutex<T>, |
src/types/mutex.rs
Outdated
/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), exposing an RAII guard interface similar to `std::sync::Mutex`. | ||
#[derive(Debug)] | ||
pub struct PyMutex<T> { | ||
_mutex: UnsafeCell<crate::ffi::PyMutex>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_mutex: UnsafeCell<crate::ffi::PyMutex>, | |
inner: UnsafeCell<crate::ffi::PyMutex>, |
To avoid a bunch of self.mutex_mutex
or self.mutex.mutex
(if the underscore were to be removed)
src/types/mutex.rs
Outdated
use std::cell::UnsafeCell; | ||
use std::ops::{Deref, DerefMut}; | ||
|
||
/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), exposing an RAII guard interface similar to `std::sync::Mutex`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should go into depth about how it's different from std's Mutex as well.
src/types/mutex.rs
Outdated
|
||
/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), exposing an RAII guard interface similar to `std::sync::Mutex`. | ||
#[derive(Debug)] | ||
pub struct PyMutex<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub struct PyMutex<T> { | |
pub struct PyMutex<T: ?Sized> { |
We should be able to support unsized types, I think.
It doesn't exist because there's no safe way to kill threads in general. This test should be in its own process (is that what integration tests are? I'm unsure). |
Thanks for the suggestions! Can you elaborate about what API surface from
Ah good point, if I write a small python wrapper I can do this using pytest. |
CodSpeed Performance ReportMerging #4523 will not alter performanceComparing Summary
|
While working on something else, I realized just now that rather than creating a new |
src/types/mutex.rs
Outdated
let first_thread_locked_once = AtomicBool::new(false); | ||
let second_thread_locked_once = AtomicBool::new(false); | ||
let finished = AtomicBool::new(false); | ||
let (sender, receiver) = sync_channel::<bool>(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a std::sync::Barrier
My view is that lock poisoning is useful for propagating panics across threads, so when writing my own code I would like to have it enabled. But I'm not sure what is best in general. |
src/types/mutex.rs
Outdated
|
||
/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), exposing an RAII guard interface. | ||
/// | ||
/// Comapred with `std::sync::Mutex` or `parking_lot::Mutex`, this is a very |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comapred -> Compared
22ee351
to
3d8e8d8
Compare
@davidhewitt and I chatted about this today and he convinced me that because PyO3 automatically converts rust panics into Python exceptions, it's actually more important to deal with poisoning, since the default behavior is not to crash the process. I'm going to take a look at extending this to use |
We also decided this doesn't need to block the 0.23 release, since the FFI wrappers also exist. |
Ref #4504 (comment) and #4265.
pyo3_ffi::PyMutex::new()
to allow pyo3 (and users?) to construct PyMutex instances.The test might not be doing anything interesting? I couldn't figure out how to re-acquire the GIL inside of
allow_threads
and the type errors I was seeing make me think it's not possible.