Вызывает ли `into_inner()` на атомарном учете все расслабленные записи?
Есть ли 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 гарантирует, что все потоки завершены, и поскольку право собственности переходит к основному потоку, я также понимаю, что не будет никаких непогашенных заимствований, но, как я вижу, все еще могут быть ожидающие незавершенные записи, если не на процессоры, то в кешах.
Какая концепция гарантирует, что все записи завершены и все кэши синхронизируются с основным потоком, когда into_inner()
называется? Можно ли потерять записи?
3 ответа
Есть ли
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 внутренне), мы можем видеть в отношении функции std::thread::join в C++ : "Завершение потока, идентифицируемого *this
синхронизируется с соответствующим успешным возвратом ". Фактически, тот же самый пример на C++ доступен на cppreference.com как часть объяснения ослабленного ограничения порядка памяти:
#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
) означает, что больше нет заимствований в этом резервном хранилище.
каждый fetch_add
атомное с Relaxed
упорядочение, поэтому, как только потоки завершены, не должно быть ничего, что изменяет это (если так, то есть ошибка в поперечной балке).
Смотрите описание на into_inner
для получения дополнительной информации