Skip to content

Commit 5728cf4

Browse files
committed
Extend the documentation on the thread-level shenanigans now employed by allow_threads.
1 parent 292bca3 commit 5728cf4

File tree

3 files changed

+69
-13
lines changed

3 files changed

+69
-13
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ chrono = { version = "0.4.25" }
5252
trybuild = ">=1.0.70"
5353
proptest = { version = "1.0", default-features = false, features = ["std"] }
5454
send_wrapper = "0.6"
55+
scoped-tls = "1.0"
5556
serde = { version = "1.0", features = ["derive"] }
5657
serde_json = "1.0.61"
5758
rayon = "1.6.1"

noxfile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ def set_minimal_package_versions(session: nox.Session):
506506
"crossbeam-deque": "0.8.3",
507507
"crossbeam-epoch": "0.9.15",
508508
"crossbeam-utils": "0.8.16",
509+
"scoped-tls": "1.0.0",
509510
}
510511

511512
# run cargo update first to ensure that everything is at highest

src/marker.rs

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
//! That API is provided by [`Python::allow_threads`] and enforced via the [`Send`] bound on the
1818
//! closure and the return type.
1919
//!
20-
//! In practice this API works quite well, but it comes with some drawbacks:
20+
//! In practice this API works quite well, but it comes with a big drawback:
21+
//! There is no instrinsic reason to prevent `!Send` types like [`Rc`] from crossing the closure.
22+
//! After all, we release the GIL to let other Python threads run, not necessarily to launch new threads.
2123
//!
22-
//! ## Drawbacks
23-
//!
24-
//! There is no reason to prevent `!Send` types like [`Rc`] from crossing the closure. After all,
25-
//! [`Python::allow_threads`] just lets other Python threads run - it does not itself launch a new
26-
//! thread.
24+
//! But to isolate the closure from references bound to the current thread holding the GIL
25+
//! and to close soundness holes implied by thread-local storage hiding such references,
26+
//! we do need to run the closure on a dedicated runtime thread.
2727
//!
2828
//! ```rust, compile_fail
2929
//! use pyo3::prelude::*;
@@ -33,12 +33,62 @@
3333
//! let rc = Rc::new(5);
3434
//!
3535
//! py.allow_threads(|| {
36-
//! // This would actually be fine...
36+
//! // This could be fine...
3737
//! println!("{:?}", *rc);
3838
//! });
3939
//! });
4040
//! ```
4141
//!
42+
//! However, running the closure on a distinct thread is required as otherwise
43+
//! thread-local storage could be used to "smuggle" GIL-bound data into it
44+
//! independently of any trait bounds (whether using `Send` or an auto trait
45+
//! dedicated to handling GIL-bound data):
46+
//!
47+
//! ```rust, no_run
48+
//! use pyo3::prelude::*;
49+
//! use pyo3::types::PyString;
50+
//! use scoped_tls::scoped_thread_local;
51+
//!
52+
//! scoped_thread_local!(static WRAPPED: PyString);
53+
//!
54+
//! fn callback() {
55+
//! WRAPPED.with(|smuggled: &PyString| {
56+
//! println!("{:?}", smuggled);
57+
//! });
58+
//! }
59+
//!
60+
//! Python::with_gil(|py| {
61+
//! let string = PyString::new(py, "foo");
62+
//!
63+
//! WRAPPED.set(string, || {
64+
//! py.allow_threads(callback);
65+
//! });
66+
//! });
67+
//! ```
68+
//!
69+
//! PyO3 tries to minimize the overhead of using dedicated threads by re-using them,
70+
//! i.e. after a thread is spawned to execute a closure with the GIL temporarily released,
71+
//! it is kept around for up to one minute to potentially service subsequent invocations of `allow_threads`.
72+
//!
73+
//! Note that PyO3 will however not wait to re-use an existing that is currently blocked by other work,
74+
//! i.e. to keep latency to a minimum a new thread will be started to immediately run the given closure.
75+
//!
76+
//! These long-lived background threads are named `pyo3 allow_threads runtime thread`
77+
//! to facilitate diagnosing any performance issues they might cause on the process level.
78+
//!
79+
//! One important consequence of this approach is that the state of thread-local storage (TLS)
80+
//! is essentially undefined: The thread might be newly spawn so that TLS needs to be newly initialized,
81+
//! but it might also be re-used so that TLS contains values created by previous calls to `allow_threads`.
82+
//!
83+
//! If the performance overhead of shunting the closure to another is too high
84+
//! or code requires access to thread-local storage established by the calling thread,
85+
//! there is the unsafe escape hatch [`Python::unsafe_allow_threads`]
86+
//! which executes the closure directly after suspending the GIL.
87+
//!
88+
//! However, note establishing the required invariants to soundly call this function
89+
//! requires highly non-local reasoning as thread-local storage allows "smuggling" GIL-bound references
90+
//! using what is essentially global state.
91+
//!
4292
//! [`Rc`]: std::rc::Rc
4393
//! [`Py`]: crate::Py
4494
use crate::err::{self, PyDowncastError, PyErr, PyResult};
@@ -232,17 +282,19 @@ impl<'py> Python<'py> {
232282
/// Temporarily releases the GIL, thus allowing other Python threads to run. The GIL will be
233283
/// reacquired when `F`'s scope ends.
234284
///
235-
/// If you don't need to touch the Python
236-
/// interpreter for some time and have other Python threads around, this will let you run
237-
/// Rust-only code while letting those other Python threads make progress.
285+
/// If you don't need to touch the Python interpreter for some time and have other Python threads around,
286+
/// this will let you run Rust-only code while letting those other Python threads make progress.
238287
///
239-
/// Only types that implement [`Send`] can cross the closure. See the
240-
/// [module level documentation](self) for more information.
288+
/// Only types that implement [`Send`] can cross the closure
289+
/// because *it is executed on a dedicated runtime thread*
290+
/// to prevent access to GIL-bound references based on thread identity.
241291
///
242292
/// If you need to pass Python objects into the closure you can use [`Py`]`<T>`to create a
243293
/// reference independent of the GIL lifetime. However, you cannot do much with those without a
244294
/// [`Python`] token, for which you'd need to reacquire the GIL.
245295
///
296+
/// See the [module level documentation](self) for more information.
297+
///
246298
/// # Example: Releasing the GIL while running a computation in Rust-only code
247299
///
248300
/// ```
@@ -409,7 +461,7 @@ impl<'py> Python<'py> {
409461

410462
/// An unsafe version of [`allow_threads`][Self::allow_threads]
411463
///
412-
/// This version does not run the given closure on a dedicated runtime thread,
464+
/// This version does _not_ run the given closure on a dedicated runtime thread,
413465
/// therefore it is more efficient and has access to thread-local storage
414466
/// established at the call site.
415467
///
@@ -436,6 +488,8 @@ impl<'py> Python<'py> {
436488
/// });
437489
/// ```
438490
///
491+
/// See the [module level documentation](self) for more information.
492+
///
439493
/// # Safety
440494
///
441495
/// The caller must ensure that no code within the closure accesses GIL-protected data

0 commit comments

Comments
 (0)