Насколько я понимаю, если два потока читают из одного и того же фрагмента памяти, и ни один поток не записывает в эту память, тогда операция безопасна. Однако я не уверен, что произойдет, если один поток читает, а другой пишет. Что случилось бы? Результат не определен? Или прочитанное будет просто устаревшим? Если устаревшее чтение не вызывает беспокойства, можно ли иметь несинхронизированное чтение-запись для переменной? Или возможно, что данные будут повреждены, и ни чтение, ни запись не будут правильными, и в этом случае всегда следует синхронизировать?
Я хочу сказать, что я узнал, что это более поздний случай, когда гонка при доступе к памяти состояние остается неопределенным ... но я не помню, где я мог это узнать, и мне трудно найти ответ в Google. Моя интуиция заключается в том, что переменная обрабатывается в регистрах, и что истинный (как в аппаратном) параллелизм невозможен (или это так), поэтому худшее, что может произойти, - это устаревшие данные, то есть следующее:
WriteThread: copy value from memory to register
WriteThread: update value in register
ReadThread: copy value of memory to register
WriteThread: write new value to memory
В какой момент поток чтения имеет устаревшие данные.
Результат не определен. Поврежденные данные вполне возможны. В качестве очевидного примера рассмотрим 64-битное значение, которым управляет 32-битный процессор. Предположим, что значение представляет собой простой счетчик, и мы увеличиваем его, когда младшие 32 бита содержат 0xffffffff. Приращение дает 0x00000000. Когда мы обнаруживаем это, мы увеличиваем старшее слово. Если, однако, какой-то другой поток считывает значение между моментом увеличения младшего слова и увеличением старшего слова, они получают значение с неувеличенным старшим словом, но младшее слово установлено в 0 — значение совершенно другое. от того, что было бы до или после завершения приращения.
Обычно память считывается или записывается в атомарных единицах, определяемых архитектурой ЦП (в наши дни 32-битные и 64-битные элементы выровнены по 32-битным и 64-битным границам).
В этом случае происходящее зависит от объема записываемых данных.
Давайте рассмотрим случай 32-битных атомарных ячеек чтения/записи.
Если два потока записывают 32 бита в такую выровненную ячейку, то абсолютно точно определено, что происходит: одно из двух записанных значений сохраняется. К сожалению для вас (ну и программы), вы не знаете какое значение. Чрезвычайно умное программирование позволяет использовать атомарность операций чтения и записи для построения алгоритмов синхронизации (например, алгоритм Деккера), но вместо этого обычно быстрее использовать архитектурно определенные блокировки.
Если два потока записывают больше, чем атомарная единица (например, они оба записывают 128-битное значение), то на самом деле части записанных значений размером в атомарную единицу будут храниться в абсолютно четко определенном способом, но вы не будете знать, какие части какого значения записываются в каком порядке. Таким образом, в хранилище может оказаться значение из первого потока, второго потока или смеси битов в атомарных единицах измерения из обоих потоков.
Аналогичные идеи справедливы для одного потока, читающего и одного потока, записывающего в атомарных единицах и больше.
В принципе, вы не хотите выполнять несинхронизированное чтение и запись в области памяти, потому что вы не будете знать результат, даже если он может быть очень хорошо определен архитектурой.
Как я намекнул в ответе Иры Бакстер, кэш процессора также играет роль в многоядерных системах. Рассмотрим следующий тестовый код:
Следующий код увеличивает приоритет в реальном времени для достижения несколько более стабильных результатов — хотя для этого требуются права администратора, будьте осторожны при запуске кода на двух- или одноядерных системах, так как ваша машина будет заблокирована на время работы. тестовый забег.
#include <windows.h>
#include <stdio.h>
const int RUNFOR = 5000;
volatile bool terminating = false;
volatile int value;
static DWORD WINAPI CountErrors(LPVOID parm)
{
int errors = 0;
while(!terminating)
{
value = (int) parm;
if(value != (int) parm)
errors++;
}
printf("\tThread %08X: %d errors\n", parm, errors);
return 0;
}
static void RunTest(int affinity1, int affinity2)
{
terminating = false;
DWORD dummy;
HANDLE t1 = CreateThread(0, 0, CountErrors, (void*)0x1000, CREATE_SUSPENDED, &dummy);
HANDLE t2 = CreateThread(0, 0, CountErrors, (void*)0x2000, CREATE_SUSPENDED, &dummy);
SetThreadAffinityMask(t1, affinity1);
SetThreadAffinityMask(t2, affinity2);
ResumeThread(t1);
ResumeThread(t2);
printf("Running test for %d milliseconds with affinity %d and %d\n", RUNFOR, affinity1, affinity2);
Sleep(RUNFOR);
terminating = true;
Sleep(100); // let threads have a chance of picking up the "terminating" flag.
}
int main()
{
SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS);
RunTest(1, 2); // core 1 & 2
RunTest(1, 4); // core 1 & 3
RunTest(4, 8); // core 3 & 4
RunTest(1, 8); // core 1 & 4
}
В моей системе с четырехъядерным процессором Intel Q6600 (у iirc есть два набора ядер, каждый из которых использует общий кэш L2 — в любом случае это объясняет результаты ;)), я получаю следующие результаты:
Running test for 5000 milliseconds with affinity 1 and 2 Thread 00002000: 351883 errors Thread 00001000: 343523 errors Running test for 5000 milliseconds with affinity 1 and 4 Thread 00001000: 48073 errors Thread 00002000: 59813 errors Running test for 5000 milliseconds with affinity 4 and 8 Thread 00002000: 337199 errors Thread 00001000: 335467 errors Running test for 5000 milliseconds with affinity 1 and 8 Thread 00001000: 55736 errors Thread 00002000: 72441 errors