标题中问题的合格答案是“是”,但我们不能通过复制非静态引用来做到这一点。这种看似限制的原因是合理的。我们可以将所需的数据/对象放入线程闭包的方法是将它们的所有权(或它们的副本,或代表它们的其他具体对象)传递给闭包。
对于如何使用像 pyo3 这样的复杂库来执行此操作可能不是很清楚,因为大部分 API 返回对象的引用类型,而不是可以按原样传递给其他线程的具体对象,但是库确实提供了将 Python 数据/对象传递给其他线程的方法,我将在下面的第二个示例中介绍这些方法。
start() 函数需要将'static 绑定到与其data 参数关联的闭包类型上,因为在其主体中,start() 将这些闭包传递给其他线程。编译器正在努力确保闭包不会保留对任何可能蒸发的引用,如果线程运行时间超过其父级,这就是它在没有'static 保证的情况下抱怨的原因。
fn start<'a>(data : Vec<Arc<dyn Fn() -> f64 + Send + Sync + 'static>>,
more_data : String)
{
for _ in 1..=4 {
let cloned_data = data.clone();
let cloned_more_data = more_data.clone();
thread::spawn(move || foo(cloned_data, cloned_more_data));
}
}
'static 绑定不同于应用于引用的'static 生命周期(data: 'static 与&'static data)。在绑定的情况下,它仅意味着它所应用的类型不包含任何非静态引用(如果它甚至包含任何引用)。 这种绑定应用于线程代码中的方法参数是很常见的。
由于这特别适用于pyo3 问题空间,我们可以通过将任何此类引用转换为拥有的对象来避免形成包含非静态引用的闭包,然后当在另一个线程中运行的回调需要执行某些操作时它们,它可以获取 GIL 并将它们转换回 Python 对象引用。
在下面的代码 cmets 中了解更多信息。我从pyo3 GitHub README 中获取了simple example,并将其与playground example 中提供的代码结合起来。
应用此模式时需要注意的一点是死锁。线程需要获取 GIL 才能使用它们有权访问的 Python 对象。在示例中,一旦父线程完成生成新线程,它会在超出范围时释放 GIL。然后,父线程通过加入它们的句柄来等待子线程完成。
use std::thread;
use std::thread::JoinHandle;
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::IntoPyDict;
use pyo3::types::PyDict;
type MyClosure<'a> = dyn Fn() -> f64 + Send + Sync + 'a;
fn main() -> Result<(), ()>
{
match Python::with_gil(|py| main_(py)
.map_err(|e| e.print_and_set_sys_last_vars(py)))
{
Ok(handles) => {
for handle in handles {
handle.join().unwrap();
}},
Err(e) => { println!("{:?}", e); },
}
Ok(())
}
fn main_(py: Python) -> PyResult<Vec<JoinHandle<()>>>
{
let sys = py.import("sys")?;
let version = sys.get("version")?.extract::<String>()?;
let locals = [("os", py.import("os")?)].into_py_dict(py);
let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'";
let user = py.eval(code, None, Some(&locals))?.extract::<String>()?;
println!("Hello {}, I'm Python {}", user, version);
// The thread will do something with the `locals` dictionary. In order to
// pass this reference object to the thread, first convert it to a
// non-reference object.
// Convert `locals` to `PyObject`.
let locals_obj = locals.to_object(py);
// Now we can move `locals_obj` into the thread without concern.
let closure: Arc<MyClosure<'_>> = Arc::new(move || {
// We can print out the PyObject which reveals it to be a tuple
// containing a pointer value.
println!("{:?}", locals_obj);
// If we want to do anything with the `locals` object, we can cast it
// back to a `PyDict` reference. We'll need to acquire the GIL first.
Python::with_gil(|py| {
// We have the GIL, cast the dict back to a PyDict reference.
let py_dict = locals_obj.cast_as::<PyDict>(py).unwrap();
// Printing it out reveals it to be a dictionary with the key `os`.
println!("{:?}", py_dict);
});
1.
});
let data = vec![closure];
let more = "Important data.".to_string();
let handles = start(data, more);
Ok(handles)
}
fn start<'a>(data : Vec<Arc<MyClosure<'static>>>,
more : String
) -> Vec<JoinHandle<()>>
{
let mut handles = vec![];
for _ in 1..=4 {
let cloned_data = data.clone();
let cloned_more = more.clone();
let h = thread::spawn(move || foo(cloned_data, cloned_more));
handles.push(h);
}
handles
}
fn foo<'a>(data : Vec<Arc<MyClosure<'a>>>,
more : String)
{
for closure in data {
closure();
}
}
输出:
Hello todd, I'm Python 3.8.10 (default, Jun 2 2021, 10:49:15)
[GCC 9.4.0]
Py(0x7f3329ccdd40)
Py(0x7f3329ccdd40)
Py(0x7f3329ccdd40)
{'os': <module 'os' from '/usr/lib/python3.8/os.py'>}
{'os': <module 'os' from '/usr/lib/python3.8/os.py'>}
{'os': <module 'os' from '/usr/lib/python3.8/os.py'>}
Py(0x7f3329ccdd40)
{'os': <module 'os' from '/usr/lib/python3.8/os.py'>}
需要考虑的一点:您可以通过将 Python 对象所需的所有信息提取到 Rust 对象中并将这些信息传递给线程来最小化或消除将它们传递给线程的需要。