Насколько низко вы спускаетесь, прежде чем что-то само станет потокобезопасным?

Я думал, насколько глубоко во всем нужно вникнуть, чтобы что-то автоматически стало потокобезопасным?

Быстрый пример:

int dat = 0;
void SetInt(int data)
{
    dat = data;
}

.. Будет ли этот метод считаться потокобезопасным? Я обычно оборачиваю все свои set-методы в мьютексы, на всякий случай, но каждый раз, когда я это делаю, я не могу не думать, что это бесполезные накладные расходы на производительность. Полагаю, все сводится к сборке, которую генерирует компилятор? Когда потоки могут проникать в код? По инструкции сборки или по строке кода? Может ли поток вмешаться во время установки или разрушения стека методов? Будет ли такая инструкция, как i ++, считаться потокобезопасной, а если нет, то как насчет ++ i?

Здесь много вопросов - и я не жду прямого ответа, но некоторая информация по этому поводу была бы отличной :)

[ОБНОВЛЕНИЕ] Поскольку мне теперь ясно (спасибо вам, ребята <3), что единственная гарантированная атомарность вещь в потоках - это инструкция по сборке, я пришел к мысли: а как насчет классов-оберток мьютексов и семафоров? Такие классы обычно используют методы, которые создают стеки вызовов - и пользовательские классы семафоров, которые обычно используют какой-то внутренний счетчик, не могут быть гарантированно атомарными / поточно-безопасными (как бы вы это ни называли, пока вы понимаете, что я имею в виду, мне все равно: П )

Обновлен исходный вопрос с продолжением :)

Meeh 12.12.2008 12:49

Что касается атомарности и инструкций по сборке, это не так. Выборки из памяти могут выполняться, когда ЦП делает что-то еще, или некоторые формы предсказания ветвления могут выполнять обе ветви и отбрасывать неправильную. x86 Есть некоторые атомарные операции, но не все.

Jasper Bekkers 12.12.2008 14:00

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

David Schwartz 31.01.2012 04:12
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
3
902
10

Ответы 10

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

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

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

Да, я так и думал ... Тогда будет ли тип данных без размера процессора, такой как double, потокобезопасным в вызове присваивания? А если int-assignment является атомарной операцией, разве у вас нет старой доброй проблемы с синхронизацией транзакций? Ой, да и проблемы с переносимостью Ну что ж, вернемся к разборкам ..

Meeh 12.12.2008 10:42

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

Konrad Rudolph 12.12.2008 11:10

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

Edouard A. 12.12.2008 12:55

> «между любыми двумя инструкциями языка ассемблера» Некоторые инструкции ассемблера (например, add) должны читать, изменять, а затем записывать обратно в эту память. Для многоядерного CPUS даже одна инструкция по сборке может быть небезопасной. Если вы не можете использовать мьютексы, вам нужно использовать cmpxchg или аналогичный встроенный компилятор.

Kevin 12.12.2008 23:21

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

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

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

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

Это потокобезопасный нет, и он подходит не для всех ситуаций.

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

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

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

James Anderson 12.12.2008 11:13

Приведенный выше код является потокобезопасным!

Главное, на что следует обратить внимание, - это статические (т. Е. Общие) переменные.

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

Поэтому, пока в вашем коде нет статических данных, он сам по себе будет потокобезопасным.

Затем вам нужно проверить, являются ли какие-либо библиотеки или системные вызовы потокобезопасными. Это явно указано в документации большинства системных вызовов.

Арргггх! Должен быть "слепой код", это вообще не потокобезопасно! Простите.

James Anderson 12.12.2008 17:36

Операция увеличения небезопасна для процессоров x86, потому что она не атомарна. В окнах вам нужно вызывать функции InterlockedIncrement. Эта функция создает препятствия для полной памяти. Также вы можете использовать tbb :: atomic из библиотеки Intel Threading Building Blocks (TBB).

Назначение «собственных» типов данных (32-битные) атомарно на большинстве платформ (включая x86). Это означает, что присвоение произойдет полностью, и вы не рискуете получить «обновленную наполовину» переменную данных. Но это единственная гарантия, которую вы получаете.

Я не уверен в назначении двойного типа данных. Вы можете посмотреть его в спецификациях x86 или проверить, дает ли .NET какие-либо явные гарантии. Но в целом типы данных, которые не являются «собственным размером», не будут атомарными. Даже меньшие, такие как bool, могут не быть (потому что для записи bool вам, возможно, придется прочитать все 32-битное слово, перезаписать один байт, а затем снова записать все 32-байтовое слово)

Как правило, потоки могут прерываться между любыми двумя инструкциями по сборке. Это означает, что приведенный выше код является потокобезопасным, если вы не пытаетесь выполнить читать из dat (что, как вы можете возразить, делает его довольно бесполезным).

Атомарность и безопасность потоков - это не совсем одно и то же. Безопасность потоков полностью зависит от контекста. Ваше присвоение dat является атомарным, поэтому другой поток, читающий значение dat, увидит либо старое, либо новое значение, но никогда не будет «промежуточным». Но это не делает его потокобезопасным. Другой поток может прочитать старое значение (скажем, это размер массива) и выполнить операцию на его основе. Но вы можете обновить dat сразу после того, как он прочитает старое значение, возможно, установив для него меньшее значение. Другой поток может теперь получить доступ к вашему новому меньшему массиву, но считает, что он имеет старый, больший размер.

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

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

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

соображения:

1) оптимизация компилятора - существует ли вообще "dat", как вы планировали? Если это поведение не является "наблюдаемым извне", абстрактная машина C / C++ не гарантирует, что компилятор не оптимизирует ее. В вашем двоичном коде может вообще не быть "dat", но вместо этого вы можете писать в регистр, и потоки будут иметь / могут иметь разные регистры. Прочтите стандарт C / C++ на абстрактной машине или просто погуглите для слова "volatile" и исследуйте его оттуда. Стандарт C / C++ заботится о работоспособности одного потока, несколько потоков могут легко споткнуться о такой оптимизации.

2) атомные накопители. Все, что имеет шанс пересечь границы слов, не будет атомарным. Int-s обычно есть, если вы не упаковываете их в структуру, которая имеет, например, символы, и директивы use для удаления отступов. Но каждый раз нужно анализировать этот аспект. Изучите свою платформу, погуглите по запросу "padding". Имейте в виду, что разные процессоры имеют разные правила.

3) проблемы с несколькими процессорами. Вы написали "dat" на CPU0. Будет ли это изменение видно даже на CPU1? Или просто в местный реестр напишете? Кешировать? Кеши поддерживаются согласованно с вашей платформой? Гарантируется ли порядок доступа? Читайте по "модели слабой памяти". Очки для "memory_barriers.txt Linux" - хорошее начало.

4) вариант использования. Вы собираетесь использовать "dat" после присваивания - это синхронизировано? Но это, я думаю, очевидно.

Обычно «потокобезопасность» не выходит за рамки гарантии того, что функция будет работать при одновременном вызове из разных потоков, но эти вызовы не должны быть взаимозависимыми, т.е. они не обмениваются какими-либо данными в отношении этого вызова. Например, вы вызываете malloc () из потоков thread1 и thread2, и они оба получают память, но не обращаются к памяти друг друга.

Контрпримером может служить strtok (), который не является потокобезопасным и не работает даже при несвязанных вызовах.

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

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

Steve Jessop 12.12.2008 15:40

В области транзакционной памяти ведется много исследований. Что-то похожее на транзакции БД, но на более мелком уровне.

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

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

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

Звучит действительно очень сексуально! Но, наверное, в будущее :(

Meeh 12.12.2008 14:08

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

Steve Jessop 12.12.2008 15:43

Другие вопросы по теме