Опасности незащищенных общих переменных в многопоточной среде - PullRequest
1 голос
/ 11 ноября 2010

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

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

Скажитечто переменная является 32-битным целым числом.Возможно ли получить мусорное чтение переменной в середине записи?Или пишите 32-битное целое число атомарной операции?Это будет зависеть от ОС или аппаратного обеспечения?Как насчет 64-битного целого числа в 32-битной системе?

А как насчет разделяемой памяти вместо потоков?

Ответы [ 6 ]

3 голосов
/ 11 ноября 2010

Запись 64-разрядного целого числа в 32-разрядной системе не является атомарной, и вы можете иметь неверные данные, если не берете блокировку.

Например, если ваше целое число равно

0x00000000 0xFFFFFFFF

и вы собираетесь написать следующее int по порядку, вы хотите написать:

0x00000001 0x00000000

Но если вы читаете значение после того, как один из целых записан, а перед другиместь, тогда вы можете прочитать

0x00000000 0x00000000

или

0x00000001 0xFFFFFFFF

, которые сильно отличаются от правильного значения.

Если вы хотите работать без блокировок, у вас естьчтобы быть очень уверенным, что представляет собой атомарную операцию в вашей комбинации ОС / ЦП / компилятор.

2 голосов
/ 11 ноября 2010

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

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

1 голос
/ 11 ноября 2010

8-битное, 16-битное или 32-битное чтение / запись гарантированно будут атомарными, если они выровнены по размеру (на 486 и позже) и не выровнены, но в пределах строки кэша (на P6 и позже).Большинство компиляторов гарантируют, что переменные стека (локальные, при условии C / C ++) выровнены.

64-разрядное чтение / запись гарантированно будет атомарным, если оно выровнено (в Pentium и более поздних версиях), однако это зависит отна компиляторе, генерирующем одну инструкцию (например, выталкивание 64-битного числа с плавающей запятой из FPU или использование MMX).Я ожидаю, что большинство компиляторов будут использовать два 32-битных доступа для совместимости, хотя, безусловно, возможно проверить (разборку) и может привести к другой обработке.

Следующая проблема - кэширование и ограждение памяти.Однако эффект от их игнорирования заключается в том, что некоторые потоки могут видеть старое значение, даже если оно было обновлено.Значение не будет недействительным, просто устарело (возможно, на микросекунды).Если это критично для вашего приложения, вам придется копать глубже, но я сомневаюсь, что это так.

(Источник: Руководство по разработке программного обеспечения Intel, том 3A )

0 голосов
/ 11 ноября 2010

Платформы часто предоставляют элементарный доступ для чтения / записи (применяется на аппаратном уровне) к примитивным значениям (32-разрядным или 64-разрядным, как в вашем примере) - см. Интерфейсы Interlocked * API в Windows .

Это может избежать использования более тяжелой блокировки веса для потоковой безопасной переменной или доступа к элементу, но не должно смешиваться с другими типами блокировки в том же экземпляре или элементе.Другими словами, не используйте Mutex для обеспечения доступа в одном месте и используйте Interlocked* для изменения или чтения в другом.

0 голосов
/ 11 ноября 2010

Платформа, на которой вы работаете, определяет размер атомарных операций чтения / записи. Как правило, 32-разрядная (регистровая) платформа поддерживает только 32-разрядные атомарные операции. Итак, если вы пишете более 32 бит, вам, вероятно, придется использовать какой-то другой механизм для координации доступа к этим общим данным.

Один механизм заключается в удвоении или тройном буфере фактических данных и использовании общего индекса для определения «последней» версии:

write(blah)
{
    new_index= ...; // find a free entry in the global_data array.
    global_data[new_index]= blah;
    WriteBarrier(); // write-release
    global_index= new_index;
}

read()
{
    read_index= global_index;
    ReadBarrier(); // read-acquire
    return global_data[read_index];
}

Вам нужны барьеры памяти, чтобы гарантировать, что вы не читаете с global_data[...] до тех пор, пока не прочитаете global_index, и не будете писать на global_index до тех пор, пока не напишите на global_data[...].

Это немного ужасно, поскольку вы также можете столкнуться с проблемой ABA с вытеснением, поэтому не используйте это напрямую.

0 голосов
/ 11 ноября 2010

Это очень сильно зависит от оборудования и от того, как вы с ним разговариваете.Если вы пишете на ассемблере, вы будете точно знать, что вы получите, поскольку руководства по процессорам подскажут, какие операции являются атомарными и при каких условиях.Например, в Intel Pentium 32-разрядные чтения являются атомарными, если адрес выровнен, но не иначе.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...