簡體   English   中英

在原子上調用`into_inner()`是否考慮了所有輕松的寫入?

[英]Does calling `into_inner()` on an atomic take into account all the relaxed writes?

into_inner()是否返回此示例程序中的所有輕松寫入? 如果是這樣,哪個概念保證了這個?

extern crate crossbeam;

use std::sync::atomic::{AtomicUsize, Ordering};

fn main() {
    let thread_count = 10;
    let increments_per_thread = 100000;
    let i = AtomicUsize::new(0);

    crossbeam::scope(|scope| {
        for _ in 0..thread_count {
            scope.spawn(|| {
                for _ in 0..increments_per_thread {
                    i.fetch_add(1, Ordering::Relaxed);
                }
            });
        }
    });

    println!(
        "Result of {}*{} increments: {}",
        thread_count,
        increments_per_thread,
        i.into_inner()
    );
}

https://play.rust-lang.org/?gist=96f49f8eb31a6788b970cf20ec94f800&version=stable

我知道crossbeam保證所有線程都已完成,並且由於所有權返回到主線程,我也明白沒有未完成的借用,但是我看到它的方式,如果沒有,仍然可能有未完成的待處理寫入CPU,然后在緩存中。

into_inner()時,哪個概念保證所有寫入都已完成,所有緩存都會同步回主線程? 是否有可能丟失寫入?

into_inner()是否返回此示例程序中的所有輕松寫入? 如果是這樣,哪個概念保證了這個?

它不是into_inner它的into_inner ,而是它的join

什么into_inner保證是,要么同步的一些已經因為最終並發寫入執行( join線程,最后Arc已被刪除並解開與try_unwrap ,等等),或原子從未在首位發送到另一個線程。 這兩種情況都足以使讀取數據無競爭。

Crossbeam 文檔明確指出在作用域末尾使用join

通過在作用域退出之前讓父線程加入子線程來確保[保證終止的線程]。

關於丟失寫入:

into_inner()時,哪個概念保證所有寫入都已完成,所有緩存都會同步回主線程? 是否有可能丟失寫入?

正如文檔中的各個 地方所述,Rust繼承了原子的C ++內存模型。 在C ++ 11及更高版本中,線程的完成來自join的相應成功返回同步 這意味着,在join完成時,連接線程執行的所有操作必須對調用join的線程可見,因此在此方案中不可能丟失寫入。

在原子方面,您可以將join視為對原子的獲取讀取,該線程在完成執行之前執行了一個發布存儲。

我將把這個答案作為對其他兩個的潛在補充。

提到的那種不一致,即在最終讀取計數器之前是否可能缺少某些寫入,這里是不可能的。 如果對值的寫入可以推遲到使用into_inner消耗之后,那么它將是未定義的行為。 然而,沒有意外的競爭條件在這個程序中,即使沒有計數器經與消耗into_inner ,甚至沒有的幫助下crossbeam范圍。

讓我們編寫一個沒有橫梁范圍的程序的新版本,以及不使用計數器的地方( 游樂場 ):

let thread_count = 10;
let increments_per_thread = 100000;
let i = Arc::new(AtomicUsize::new(0));
let threads: Vec<_> = (0..thread_count)
    .map(|_| {
        let i = i.clone();
        thread::spawn(move || for _ in 0..increments_per_thread {
            i.fetch_add(1, Ordering::Relaxed);
        })
    })
    .collect();

for t in threads {
    t.join().unwrap();
}

println!(
    "Result of {}*{} increments: {}",
    thread_count,
    increments_per_thread,
    i.load(Ordering::Relaxed)
);

這個版本仍然很好用! 為什么? 因為在結束線程與其對應的join之間建立了同步關系。 因此,如在單獨的答案中解釋的那樣,聯接線程執行的所有操作必須對調用者線程可見。

人們可能也想知道,即使是寬松的內存排序約束是否足以保證完整程序的行為符合預期。 這部分由Rust Nomicon解決 ,強調我的:

輕松的訪問是絕對最弱的。 它們可以自由地重新排序,並且不會在之前發生關系。 盡管如此, 放松的操作仍然是原子的 也就是說,它們不算作數據訪問,並且對它們執行的任何讀 - 修改 - 寫操作都是以原子方式進行的。 輕松的操作適合您絕對想要發生的事情,但不要特別注意。 例如,如果您沒有使用計數器同步任何其他訪問,則可以使用寬松的fetch_add通過多個線程安全地完成遞增計數器

提到的用例正是我們在這里所做的。 每個線程不需要觀察遞增的計數器以做出決定,但所有操作都是原子的。 最后,線程join與主線程同步,從而暗示先發生關系,並保證操作在那里可見。 由於Rust采用與C ++ 11相同的內存模型(這是由LLVM內部實現的),我們可以看到關於C ++ std :: thread :: join函數“由*this標識的線程完成與此對應的同步成功回歸“ 實際上, cppreference.com中提供了C ++中的相同示例,作為輕松內存順序約束的解釋的一部分:

#include <vector>
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> cnt = {0};

void f()
{
    for (int n = 0; n < 1000; ++n) {
        cnt.fetch_add(1, std::memory_order_relaxed);
    }
}

int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f);
    }
    for (auto& t : v) {
        t.join();
    }
    std::cout << "Final counter value is " << cnt << '\n';
}

您可以調用into_inner (使用AtomicUsize )這一AtomicUsize意味着該后備存儲不再需要借用。

每個fetch_add都是一個帶有Relaxed排序的原子,所以一旦線程完成就不應該有任何改變它的東西(如果是這樣的話,那么橫梁中就有一個bug)。

有關詳細信息,請參閱into_inner的說明

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM