Состояние гонки и разблокированная запись - PullRequest
2 голосов
/ 03 апреля 2012

У меня есть вопрос об условиях гонки и одновременной записи.

У меня есть класс, к которому обращаются объекты из разных потоков. Я хотел бы рассчитать некоторые значения только по требованию и кешировать результат. Из соображений производительности я бы предпочел не использовать блокировки (прежде чем кто-либо спросит - да, это актуально в моем случае).

Это составляет состояние гонки. Тем не менее, объекты являются постоянными и не будут изменены. Так что, если разные потоки вычисляют значения для кэширования, они в моем случае использования гарантированно идентичны. Будет ли безопасно записать эти значения без блокировки? Или, в более широком смысле, безопасно ли записывать идентичный контент в память из разных потоков без блокировки?

Записанные значения относятся к типу bool и double, а рассматриваемая архитектура может быть x86 и ARM.

РЕДАКТИРОВАТЬ: Спасибо всем за их вклад. Я наконец решил найти способ, который не включает в себя кеширование. Этот подход очень похож на хак, и есть проблема с использованием переменной-флага.

Ответы [ 4 ]

4 голосов
/ 04 апреля 2012

Как вы говорите, это условие гонки. В C ++ 11 это технически гонка данных и неопределенное поведение. Неважно, что значения одинаковы.

Если ваш компилятор поддерживает его (например, недавний gcc, или gcc или MSVC с моей библиотекой Just :: Thread ), тогда вы можете использовать std::atomic<some_pod_struct>, чтобы обеспечить атомарную оболочку вокруг ваших данных (при условии, что это - это структура POD - если нет, то у вас есть большие проблемы). Если он достаточно мал, то компилятор освободит его от блокировки и будет использовать соответствующие атомарные операции. Для больших структур библиотека будет использовать блокировку.

Проблема с выполнением этого без атомарных операций или блокировок - visibility . Хотя на уровне процессора на x86 или ARM нет проблем с записью одних и тех же данных (при условии, что они действительно идентичны побайтно) из нескольких потоков / процессоров в одну и ту же память, учитывая, что это кэш, я ожидаю Вы захотите прочитать эти данные, а не пересчитать их, если они уже были записаны. Поэтому вам понадобится какой-то флаг, чтобы указать готовность. Если вы не используете атомарные операции, блокировки или подходящие инструкции по блокировке памяти, тогда флаг «готовности» может стать видимым для другого процессора до того, как сделает данные. Это тогда действительно испортит ситуацию, поскольку второй процессор теперь читает неполный набор данных.

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

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

1 голос
/ 03 апреля 2012

Вам не нужно защищать от записи в переменные POD из разных потоков, если значения будут одинаковыми.Однако, если у вас есть указатели, вам определенно следует выполнить обмен с блокировкой.

ОБНОВЛЕНИЕ: Чтобы уточнить, для вашей ситуации кэширование и оптимизация не будут иметь никаких негативных последствий, поскольку вы пишете одно и то же значение для всехпотоки.По той же причине вам не нужно делать переменную volatile.Единственная вещь, которая потенциально может быть проблемой, - это если ваша переменная не выровнена по размеру слова машины.Смотрите https://stackoverflow.com/a/54242/677131 для более подробной информации.По умолчанию переменные автоматически выравниваются, но вы можете явно изменить выравнивание.

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

1 голос
/ 04 апреля 2012

Я должен начать с того, что использование блокировки обычно является правильным способом сделать это, но ...

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

Таким образом, если есть гарантия того, что результаты вычислений всегда будут одинаковыми, независимо от того, какой поток, то это не представляет опасности для нескольких потоков. Просто проверьте флаг («уже вычислен?») Перед выполнением расчета. Несколько потоков будут вводить код вычисления значения, но как только это будет сделано, конечно, никакие другие потоки не будут делать это больше. Очевидно, что делать то же самое n-раз - пустая трата времени. Вопрос в том, спасет ли использование блокировки вас в любое время или наоборот? Только тестирование производительности может дать вам ответ. Если нет других причин не использовать блокировки.

1 голос
/ 03 апреля 2012

В конечном итоге ответ, вероятно, будет зависеть от вашей структуры данных.

В «непереносимой» области вы можете захотеть взглянуть на сравнить и поменять местами , что большинство процессоров позволит вам сделать с объектом размером с указатель. Для этого вы можете использовать встроенную сборку (на x86 это инструкции lock cmpxchg) или, возможно, расширения синхронизации GCC. Увидев неинициализированное значение, каждый поток может с готовностью инициализировать и выполнить сравнение и обмен, чтобы попытаться установить значение. Если сравнение и замена не удаются, это означает, что другой поток побил вас.

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

...