【问题标题】:Is there a way of spawning new threads with copies of existing data that have a non-static lifetime?有没有办法用具有非静态生命周期的现有数据的副本生成新线程?
【发布时间】:2021-07-18 07:07:38
【问题描述】:

我有一个与Is there a succinct way to spawn new threads with copies of existing data? 中讨论的问题类似的问题。与链接的问题不同,我正在尝试将具有关联生命周期的对象移动到新线程中。

直观地说,我想做的是将继续计算所需的所有内容复制到新线程并退出旧线程。但是,当尝试将克隆数据(具有生命周期)移动到新线程时,出现以下错误:

错误[E0759]: data 具有生命周期 'a 但它需要满足 'static 生命周期要求

我根据引用的问题here 创建了一个可重现的示例。这只是为了举例说明问题。在这里,可以轻松删除生命周期,但在我的实际用例中,我想要移动到线程的数据要复杂得多。

有没有一种简单的方法可以使用 Rust 进行这项工作?

【问题讨论】:

  • 移除你奇怪的一生? play.rust-lang.org/…
  • @Stargateur 操场上的例子只是为了说明生命周期的问题。在我的实际用例中,我想传递给线程的数据要复杂得多。对不起!应该在问题中更清楚地说明这一点。
  • 使用Arc 的全部意义在于删除生命周期并使对象拥有(又名T: 'static)。如果您仍然拥有一生,则意味着您并没有真正“复制继续计算所需的所有内容”。如果您的代码已编译,则附加生命周期的对象可能会在线程仍在运行时被销毁。恐怕您需要提供更多详细信息才能获得有意义的答案。
  • 请注意,克隆对引用没有帮助,因为clone() 必须返回与原始值相同类型的值。如果原始值包含引用(例如,它是 struct Foo { x: String, y: &u32 } 之类的类型),则克隆别无选择,只能复制相同的引用。如果它克隆了基础值并在克隆中使用了对新值的引用(例如,如果Foo::clone() 创建了一个新的u32 并在它返回的Foo 中引用它),谁将拥有该值以及何时拥有该值它被丢弃了吗?
  • 阅读 pyo3 文档,我发现您可以通过 .to_object()PyAny 引用转换为具体的 PyObjectdoc says 可以安全地在线程之间传递。为什么不这样做...

标签: multithreading rust


【解决方案1】:

标题中问题的合格答案是“是”,但我们不能通过复制非静态引用来做到这一点。这种看似限制的原因是合理的。我们可以将所需的数据/对象放入线程闭包的方法是将它们的所有权(或它们的副本,或代表它们的其他具体对象)传递给闭包。

对于如何使用像 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&amp;'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 对象中并将这些信息传递给线程来最小化或消除将它们传递给线程的需要。

【讨论】:

  • 这个问题是它不适用于 OP,因为他们不希望 start() 单独编译,而是能够使用捕获的闭包来调用它非静态生命周期('py 生命周期,代表所持有的 Python GIL)。
  • 我相信这实际上可能是解决问题后的解决方案,通过在 PyAny 引用上使用 .to_object() 来消除闭包持有的非静态引用。 @user4815162342
猜你喜欢
  • 1970-01-01
  • 2019-08-05
  • 1970-01-01
  • 2020-03-18
  • 2015-07-10
  • 1970-01-01
  • 2015-01-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多