Потокобезопасность одновременного обновления переменной до одного и того же значения - PullRequest
1 голос
/ 15 января 2009

Является ли следующая конструкция поточно-ориентированной, если предположить, что элементы foo выровнены и имеют правильный размер, чтобы не было разрыва слова? Если нет, то почему нет?

Примечание: приведенный ниже код является игрушечным примером того, что я хочу сделать, а не моим реальным сценарием реального мира. Очевидно, в моем примере есть лучшие способы кодирования наблюдаемого поведения.

uint[] foo;
// Fill foo with data.

// In thread one:
for(uint i = 0; i < foo.length; i++) {
     if(foo[i] < SOME_NUMBER) {
         foo[i] = MAGIC_VAL;
     }
}

// In thread two:
for(uint i = 0; i < foo.length; i++) {
     if(foo[i] < SOME_OTHER_NUMBER) {
         foo[i] = MAGIC_VAL;
     }
}

На первый взгляд это выглядит небезопасно, поэтому я выделю, почему я думаю, что это может быть безопасно:

  1. Единственные две опции - чтобы элемент foo был неизменным или был установлен в MAGIC_VAL.
  2. Если поток два видит foo [i] в ​​промежуточном состоянии во время его обновления, могут произойти только две вещи: промежуточное состояние <<code>SOME_OTHER_NUMBER или нет. Если это <<code>SOME_OTHER_NUMBER, второй поток также попытается установить его в MAGIC_VAL. Если нет, поток два ничего не будет делать.

Редактировать: Кроме того, что, если foo это long, double или что-то, так что обновление не может быть выполнено атомарно? Вы все еще можете предполагать, что выравнивание и т. Д. Таково, что обновление одного элемента foo не повлияет на любой другой элемент. Кроме того, весь смысл многопоточности в этом случае заключается в производительности, поэтому любой тип блокировки может победить это.

Ответы [ 5 ]

4 голосов
/ 15 января 2009

На современном многоядерном процессоре ваш код НЕ является потокобезопасным (по крайней мере, в большинстве языков) без барьера памяти. Проще говоря, без явных барьеров каждый поток может видеть совершенно другую копию foo из кэшей.

Скажем, что ваши два потока выполнялись в определенный момент времени, затем в какой-то более поздний момент времени третий поток прочитал foo, он мог видеть полностью неинициализированный foo, или foo любого из двух других потоков, или в зависимости от того, что произошло с кэшированием памяти процессора.

Мой совет - не пытайтесь быть «умными» в отношении параллелизма, всегда старайтесь быть «безопасными». Смарт будет кусать тебя каждый раз. В статье сломанная двойная проверка блокировки есть некоторые откровенные сведения о том, что может случиться с доступом к памяти и переупорядочением команд при отсутствии барьеров памяти (хотя конкретно о Java и ее (изменяющейся) модели памяти, она проницательна для любого языка).

Вы должны быть действительно на вершине указанной модели памяти вашего языка для сокращения барьеров. Например, Java позволяет помечать переменную как volatile, которая в сочетании с типом, который задокументирован как имеющий атомарное назначение, может разрешать несинхронизированное назначение и выборку, форсируя их через основную память (таким образом, поток не наблюдает / обновляет кэшированные копии) .

1 голос
/ 15 января 2009

Вы можете сделать это безопасно и без блокировок с помощью операции сравнения и замены. То, что у вас есть , выглядит поточно-безопасным , но при некоторых обстоятельствах компилятор может создать обратную запись неизмененного значения, что приведет к переходу одного потока на другой.

Кроме того, вы, вероятно, не получаете столько производительности, сколько думаете, потому что наличие обоих потоков, записывающих в одну и ту же непрерывную память, как это, вызовет бурю переходов MESI внутри кеша процессора каждый из которых довольно медленный. Для получения более подробной информации о согласованности многопоточной памяти вы можете обратиться к разделу 3.3.4 Ульриха Дреппера « Что каждый программист должен знать о памяти ».

0 голосов
/ 15 января 2009

Это плохая практика, вы никогда не должны находиться в состоянии, когда два потока обращаются к одной и той же переменной одновременно, независимо от последствий. Пример, который вы приводите, слишком упрощен, с большинством сложных примеров почти всегда будут связаны проблемы .. ...

Помните: Семафоры ваш друг!

0 голосов
/ 15 января 2009

Этот конкретный пример является поточно-ориентированным.

Здесь нет действительно вовлеченных промежуточных состояний. Эта конкретная программа не запутается.

Я бы предложил Mutex для массива, хотя.

0 голосов
/ 15 января 2009

Если чтение и запись в каждый элемент массива являются атомарными (то есть они правильно выровнены без разрыва слов, как вы упоминали), тогда в этом коде не должно быть никаких проблем. Если foo[i] меньше, чем SOME_NUMBER или SOME_OTHER_NUMBER, то хотя бы один поток (возможно, оба) в какой-то момент установит для него значение MAGIC_VAL; в противном случае он останется нетронутым. С атомарным чтением и записью других возможностей нет.

Однако, поскольку ваша ситуация более сложна, будьте очень и очень осторожны - убедитесь, что foo[i] действительно читается только один раз за цикл и сохраняется в локальной переменной. Если вы читаете его более одного раза за одну и ту же итерацию, вы можете получить противоречивые результаты. Даже малейшее изменение, внесенное в код, может сразу же сделать его небезопасным в условиях гонки, поэтому комментируйте код с большими красными предупреждающими знаками.

...