Подтвердить что ты не робот

Почему ключевое слово volatile не требуется для синхронизации потоков?

Я читаю, что ключевое слово volatile не подходит для синхронизации потоков, и на самом деле оно не требуется для этих целей вообще.

Хотя я понимаю, что использование этого ключевого слова недостаточно, я не понимаю, почему это совершенно не нужно.

Например, предположим, что у нас есть два потока, поток A, который читает только из общей переменной и потока B, который только записывает в общую переменную. Правильная синхронизация, например. mutexs pthreads принудительно.

IIUC, без ключевого слова volatile, компилятор может посмотреть код потока A и сказать: "Здесь переменная не изменяется, но мы имеем много чтений, давайте ее читаем только один раз, кешируем значение и оптимизируйте все последующие чтения". Также он может посмотреть код потока B и сказать: "У нас здесь много записей этой переменной, но не читается, поэтому письменные значения не нужны и, следовательно, позволяют оптимизировать все записи".

Обе оптимизации были бы неверными. И как один будет предотвращен изменчивостью. Поэтому я бы, скорее всего, пришел к выводу, что пока volatile недостаточно для синхронизации потоков, все равно необходимо, чтобы любая переменная была разделена между потоками. (примечание: теперь я читаю, что на самом деле для volatile не требуется, чтобы предотвратить ошибки написания, поэтому я не в курсе, как предотвратить такую ​​неправильную оптимизацию)

Я понимаю, что я здесь неправ. Но почему?

4b9b3361

Ответ 1

Например, предположим, что у нас есть два потока, поток A, который читает только из общей переменной и потока B, который только записывает в общую переменную. Правильная синхронизация, например. mutexs pthreads принудительно.

IIUC, без ключевого слова volatile, компилятор может посмотреть код потока A и сказать: "Здесь переменная не изменяется, но мы имеем много чтений, давайте ее читаем только один раз, кешируем значение и оптимизируйте все последующие чтения". Также он может посмотреть код потока B и сказать: "У нас здесь много записей этой переменной, но не читается, поэтому письменные значения не нужны и, следовательно, позволяют оптимизировать все записи".

Как и большинство примитивов синхронизации потоков, операции mutex pthreads имеют явно определенную семантику видимости памяти.

Либо платформа поддерживает pthreads, либо нет. Если он поддерживает pthreads, он поддерживает мьютексы pthreads. Либо эти оптимизации безопасны, либо нет. Если они в безопасности, проблем нет. Если они небезопасны, любая платформа, которая их делает, не поддерживает мьютексы pthreads.

Например, вы говорите: "Здесь переменная не изменяется, но она делает - другой поток может ее изменить. Если компилятор не может доказать, что его оптимизация не может нарушить какую-либо соответствующую программу, она не сможет это сделать. И соответствующая программа может модифицировать переменную в другом потоке. Либо компилятор поддерживает потоки POSIX, либо нет.

Как это бывает, большинство из них происходит автоматически на большинстве платформ. Компилятору просто не дают понять, что делают операции мьютекса внутри. Все, что мог сделать другой поток, могут выполнять сами операции mutex. Поэтому компилятор должен "синхронизировать" память перед входом и выходом из этих функций. Например, он не может хранить значение в регистре по вызову pthread_mutex_lock, потому что для всего, что он знает, pthread_mutex_lock обращается к этому значению в памяти. В качестве альтернативы, если компилятор имеет специальные знания о функциях мьютекса, это будет включать в себя знание о недействительности значений кеширования, доступных для других потоков в этих вызовах.

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

Как вы, наверное, слышали много раз, семантика volatile, как указано на языке C, просто не смешивается с потоками. Мало того, что этого недостаточно, он отключает многие совершенно безопасные и практически необходимые оптимизации.

Ответ 2

Сокращая уже указанный ответ, вам не нужно использовать volatile с мьютексами по простой причине:

  • Если компилятор знает, какие операции мьютекса (распознавая функции pthread_ * или потому, что вы использовали std::mutex), он хорошо знает, как обрабатывать доступ в отношении оптимизации (что даже требуется для std::mutex)
  • Если компилятор не распознает их, функции pthread_ * полностью непрозрачны для него, и никакие оптимизации, связанные с любыми нелокальными объектами продолжительности, могут пересекать непрозрачные функции

Ответ 3

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

Реализация, гарантирующая, что это зависит от архитектуры/компилятора.