#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
#include <mutex>
using namespace std::chrono;
const int nthreads = 4;
const int64_t ndata = 1600000;
int total_sum = 0;
void compute_sum(int tid, std::vector<int>& d)
{
int st = (ndata / nthreads) * tid;
int en = (ndata / nthreads) * (tid + 1);
for (int i = st; i < en; i++) {
total_sum += d[i];
}
}
int main(int argc, char ** arg)
{
std::vector<std::thread> threads;
std::vector<int> data;
for (int i = 0; i < ndata; i++) {
data.push_back(i);
}
for (int i = 0; i < nthreads; i++) {
threads.push_back(std::thread(compute_sum, i, std::ref(data)));
}
for (auto &th : threads) {
th.join();
}
return 0;
}
Здесь total_sum
используется двумя потоками, поэтому возникает состояние гонки, и этот код испорчен (пока не используются атомы или блокировки).
Но когда когерентность на аппаратном уровне реализуется по принципу протокола MESI, не должна ли общая память правильно обрабатываться самим оборудованием, чтобы не возникало состояний гонки?
Так что просто используйте atomic<int>
и доверьте реализацию, которая сделает абсолютно минимум, необходимый для атомарности обновлений. Я уверен, что разработчик библиотеки также знает о MESI.
Вы предполагаете, что total_sum += d[i]
означает, что в точном порядке исходного кода компилятор выдаст одну единственную инструкцию, которая загружает, добавляет, а затем сохраняет значение total_sum
из/в память. Но когда вы пишете на языке высокого уровня, таком как C++, код, который вы пишете, не имеет такого соответствия. Нет никаких причин, по которым компилятор должен компилировать его именно так. Например, гораздо разумнее (и разрешено в соответствии с правилами абстрактной машины) хранить total_sum
в реестре, чтобы поведение аппаратного кэша вообще не имело значения.
Также (все вышеперечисленное плюс) при отсутствии ограничений памяти оптимизатор позволяет модифицировать код так, как если бы он имел только один поток выполнения.
Протокол MESI (Википедия) — это механизм, который повышает производительность поддержания согласованности кэша ЦП.
Кэш ЦП (Википедия) — это аппаратный компонент, который находится между ЦП и основной памятью компьютера.
Согласованность кэша ЦП совершенно не связана с потокобезопасностью:
std::vector<>
, которые может использовать ваш код, илиДругими словами, одно не имеет абсолютно ничего общего с другим.
Знание о существовании кэша ЦП и его внутренней работе полезно опытным программистам только как средство написания кода, который будет работать более оптимально за счет учета особенностей работы кэша. (Например, при переборе всех элементов двумерного массива, объявленного как Array[Y][X]
, вы обычно добьетесь большей производительности, если ваш внешний цикл будет выполнять итерацию по Y, а внутренний цикл — по X.)
Я предположил следующее: «Предположим, что процессор имеет два ядра, и поток 1 выполняется на ядре 1, а поток 2 — на ядре 2. Допустим, первоначально total_sum извлекается из памяти, к нему обращается поток 1, а затем записывается обратно. а затем доступен потоку 2 и т. д. Разве протоколы когерентности не заботятся об этой синхронизации». Именно это я имел в виду, когда задавал вопрос. Понятия не имею, какой ответ вы дали :(
У каждого потока не всегда будет назначенное ядро. Планировщик операционной системы будет постоянно переназначать потоки различным ядрам.
total_sum
не обязательно ответят; как указывали другие, его можно хранить в реестре. Чтобы это произошло, вам нужно объявить это volatile
или позаботиться о синхронизации программно.
Если и когда total_sum
в конечном итоге будет записан обратно в память, и если он будет записан в кеш, который не является общим для всех ядер (некоторые кеши являются общими для нескольких ядер в некоторых архитектурах), тогда протоколы когерентности позаботятся о том, чтобы это было видно. правильным ядром, которое должно его прочитать, но опять же, вы ничего не можете сделать, чтобы облегчить или предотвратить это со стороны C++, и поэтому вас как программиста C++ это не волнует.
Это похоже на беспокойство о квантовых взаимодействиях, когда все, что вы пытаетесь сделать, — это играть в футбол. Да, конечно, на низком уровне всё сводится к квантовым взаимодействиям, но думать в терминах квантов при игре в футбол совершенно бесполезно; подойдет простая ньютоновская механика. Когда вы играете в футбол, вас не волнует квантовая механика, и нет никаких знаний о квантовой механике, которые либо улучшат, либо испортят вашу футбольную игру.
Другими словами, было время, когда процессоры не имели кэша L1, L2, L3 и не существовало протоколов когерентности, и наше программное обеспечение работало. Затем однажды производители процессоров представили кэши процессоров. Для этого не потребовалось переписывать какое-либо программное обеспечение: все, что раньше работало, продолжало работать, а все, что раньше глючило, продолжало так же глючить.
Спасибо!!!!. Я не думаю о квантовых взаимодействиях, я просто хотел знать, почему здесь вообще возникает состояние гонки. (Мой профессор высказал эту идею в классе). В любом случае, я удовлетворен ответом.
Большой разрыв между абстрактной машиной C++ и физическим оборудованием заключается в том, что без атомов или ограничений памяти абстрактная машина может предполагать, что переменные не видны ни из чего, кроме текущего потока. Это означает, что компилятору разрешено генерировать код, который считывает total_sum
в регистр, выполняет все операции суммирования в этом регистре, а затем после цикла фиксирует значение обратно в память.
Когда это происходит, все остальные операции записи, которые могли произойти в памяти между чтением и обратной записью, не видны/перезаписываются.
После добавления атомов операции должны всегда работать с памятью и быть видимыми во всем мире, как только они происходят. Если вы добавляете мьютексы или другую блокировку, то потоки должны фиксировать свои результаты обратно в память перед снятием блокировки и должны читать из памяти после получения блокировки, обеспечивая глобально согласованную последовательность операций.
Тот факт, что аппаратное обеспечение имеет инструкции и протоколы, которые обрабатывают разные вещи, не означает, что компилятор генерирует код, реализующий эти механизмы, если только это не требуется на основе используемых языковых функций.
C++ не заботится о базовой машине. C++ определяет абстрактную машину, и ваш код должен компилироваться в соответствии с правилами этой машины. Одно из этих правил заключается в том, что более одного потока не могут изменять общую переменную, если у вас нет синхронизации. Если этого не сделать, это приведет к гонке данных и неопределенному поведению.