【问题标题】:Per-thread initialization in RayonRayon 中的每线程初始化
【发布时间】:2017-07-27 15:18:18
【问题描述】:

我正在尝试使用 Rayon 的 par_iter() 优化我的功能。

单线程版本类似于:

fn verify_and_store(store: &mut Store, txs: Vec<Tx>) {

    let result = txs.iter().map(|tx| {

         tx.verify_and_store(store)

    }).collect();

    ...
}

每个Store实例只能被一个线程使用,但是Store的多个实例可以同时使用,所以我可以通过clone-ing store来实现这个多线程:

fn verify_and_store(store: &mut Store, txs: Vec<Tx>) {

    let result = txs.par_iter().map(|tx| {

         let mut local_store = store.clone();

         tx.verify_and_store(&mut local_store)

    }).collect();

    ...
}

但是,这会在次迭代中克隆store,这太慢了。我想每个线程使用一个商店实例。

人造丝可以做到这一点吗?还是我应该求助于手动线程和工作队列?

【问题讨论】:

    标签: multithreading rust rayon


    【解决方案1】:

    可以使用线程局部变量来确保local_store 在给定线程中不会被多次创建。

    例如,这会编译 (full source):

    fn verify_and_store(store: &mut Store, txs: Vec<Tx>) {
        use std::cell::RefCell;
        thread_local!(static STORE: RefCell<Option<Store>> = RefCell::new(None));
    
        let mut result = Vec::new();
    
        txs.par_iter().map(|tx| {
            STORE.with(|cell| {
                let mut local_store = cell.borrow_mut();
                if local_store.is_none() {
                    *local_store = Some(store.clone());
                }
                tx.verify_and_store(local_store.as_mut().unwrap())
            })
        }).collect_into(&mut result);
    }
    

    然而,这段代码有两个问题。第一,如果store 的克隆需要在par_iter() 完成时做一些事情,例如刷新它们的缓冲区,那根本不会发生——它们的Drop 只会在Rayon 的工作线程退出时被调用,即使这样is not guaranteed.

    第二个也是更严重的问题是store 的克隆在每个工作线程中只创建一次。如果 Rayon 缓存了它的线程池(我相信确实如此),这意味着以后对verify_and_store 的无关调用将继续使用store 的最后一个已知克隆,这可能与当前存储无关。

    这可以通过稍微复杂化代码来纠正:

    • 将克隆的变量存储在Mutex&lt;Option&lt;...&gt;&gt; 而不是Option 中,以便调用par_iter() 的线程可以访问它们。这将在每次访问时产生一个互斥锁,但该锁将是无争议的,因此很便宜。

    • 在互斥体周围使用Arc 以收集对向量中创建的存储克隆的引用。此向量用于在迭代完成后通过将它们重置为 None 来清理存储。

    • 将整个调用包装在一个不相关的互斥体中,以便对verify_and_store 的两个并行调用最终不会看到彼此的存储克隆。 (如果在迭代之前创建并安装了新的线程池,这可能是可以避免的。)希望这种序列化不会影响verify_and_store 的性能,因为每次调用都会使用整个线程池。

    结果并不漂亮,但它可以编译,只使用安全代码,而且似乎可以工作:

    fn verify_and_store(store: &mut Store, txs: Vec<Tx>) {
        use std::sync::{Arc, Mutex};
        type SharedStore = Arc<Mutex<Option<Store>>>;
    
        lazy_static! {
            static ref STORE_CLONES: Mutex<Vec<SharedStore>> = Mutex::new(Vec::new());
            static ref NO_REENTRY: Mutex<()> = Mutex::new(());
        }
        thread_local!(static STORE: SharedStore = Arc::new(Mutex::new(None)));
    
        let mut result = Vec::new();
        let _no_reentry = NO_REENTRY.lock();
    
        txs.par_iter().map({
            |tx| {
                STORE.with(|arc_mtx| {
                    let mut local_store = arc_mtx.lock().unwrap();
                    if local_store.is_none() {
                        *local_store = Some(store.clone());
                        STORE_CLONES.lock().unwrap().push(arc_mtx.clone());
                    }
                    tx.verify_and_store(local_store.as_mut().unwrap())
                })
            }
        }).collect_into(&mut result);
    
        let mut store_clones = STORE_CLONES.lock().unwrap();
        for store in store_clones.drain(..) {
            store.lock().unwrap().take();
        }
    }
    

    【讨论】:

    • 很遗憾,这个调用似乎没有任何作用域(尽管这在相当一部分情况下显然很有用)。
    • @ChrisEmerson 是的,我担心这个答案是我想不出一种方法来清理创建的商店(或在一切完成后运行其他任意命令,例如将它们刷新到磁盘)使用安全代码。更糟糕的是,下一次调用 verify_and_store 将继续使用 last 已知的 Store 克隆,这可能与当前的 store 无关。
    • 谢谢。这可行,但在我的特殊情况下,我发现 Rayon 有 par_chunks 来减少克隆的数量。尽管这可能仍会导致每个线程有多个克隆,但它不存在@user4815162342 所描述的范围问题。
    • @ChrisEmerson 我现在更新了答案以添加范围和适当的清理;代码并不优雅(至少可以说),但它似乎工作!
    • @Tomas 您可能希望将您的解决方案添加为答案,并接受它。我的回答是对 Rust/Rayon 对线程本地代码的支持进行有趣的探索,而您的 par_chunks 解决方案似乎可以很好地解决实际问题。
    【解决方案2】:

    老问题,但我觉得答案需要重新审视。一般来说,有两种方法:

    使用map_with。每次线程从另一个线程窃取工作项时,这将克隆。这可能会克隆比线程更多的商店,但它应该相当低。如果克隆成本太高,您可以使用with_min_len 增加 rayon 将工作负载拆分的大小。

    fn verify_and_store(store: &mut Store, txs: Vec<Tx>) {
        let result = txs.iter().map_with(|| store.clone(), |store, tx| {
             tx.verify_and_store(store)
        }).collect();
        ...
    }
    

    或者使用thread_local crate 中的作用域ThreadLocal。这将确保您只使用与线程一样多的对象,并且一旦ThreadLocal 对象超出范围,它们就会被销毁。

    fn verify_and_store(store: &mut Store, txs: Vec<Tx>) {
        let tl = ThreadLocal::new();
        let result = txs.iter().map(|tx| {
             let store = tl.get_or(|| Box::new(RefCell::new(store.clone)));
             tx.verify_and_store(store.get_mut());
        }).collect();
        ...
    }
    

    【讨论】:

    • 不错的答案,我现在才看到。对传递给ThreadLocal::get_or()的闭包返回的RefCell进行装箱的原因是什么?
    猜你喜欢
    • 2021-06-17
    • 1970-01-01
    • 1970-01-01
    • 2012-05-26
    • 2017-07-30
    • 2011-11-17
    • 2012-10-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多