Как низко вы идете, прежде чем что-то становится потокобезопасным само по себе?

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

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

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

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

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

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

12.12.2008 07:28:49
Обновлен оригинальный вопрос с последующей деятельностью :)
Meeh 12.12.2008 09:49:41
Что касается атомарности и инструкции по сборке, это не так. Выборки из памяти могут выполняться, когда процессор выполняет что-то еще, или некоторые формы предсказания ветвлений могут выполнять обе ветви и отбрасывать неправильные. x86 Имеет некоторые атомарные операции, но не все они есть.
Jasper Bekkers 12.12.2008 11:00:25
Ответ из учебника заключается в том, что это зависит от стандарта потоков, которому вы следуете, что обычно обеспечивает определенный набор гарантий. В случае потоков POSIX, если у вас нет элементарных операций, предоставляемых вашим компилятором, библиотеками платформы или более новым языковым стандартом, вы не можете изменять какой-либо объект, пока другой поток имеет или может обращаться к нему ... точка.
David Schwartz 31.01.2012 00:12:32
10 ОТВЕТОВ

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

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

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

3
12.12.2008 07:33:08
Да, я так и думал. Будет ли тип данных не размера процессора, такой как double, быть потокобезопасным при вызове присваивания? А если int-присваивание является атомарной операцией, разве у вас нет проблем с синхронизацией транзакций? О, мой, и проблемы с переносимостью О, хорошо, я думаю, что снова вернуться к рассмотрению
Meeh 12.12.2008 07:42:14
Грег, ты забыл, что член (предполагая, что этот сеттер находится в классе) должен быть разыменован. Насколько я знаю (и мало что знаю), результирующая операция не всегда атомарна.
Konrad Rudolph 12.12.2008 08:10:56
Мало того, что операция не является атомарной (и размер слова не является аргументом), но она не ограничена. Либо вы используете конкретные ключевые слова, чтобы гарантировать атомарность, либо вы используете мьютексы и тому подобное.
Edouard A. 12.12.2008 09:55:54
> «между любыми двумя инструкциями на языке ассемблера» Некоторые инструкции по сборке (например, add) должны прочитать, изменить, а затем записать обратно в эту память. Для многоядерного CPUS даже одна инструкция по сборке может быть небезопасной. Если вы не можете использовать мьютексы, тогда вам нужно использовать 'cmpxchg' или аналогичный встроенный компилятор.
Kevin 12.12.2008 20:21:55

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

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

2
12.12.2008 07:35:32

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

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

3
12.12.2008 07:54:09

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

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

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

2
12.12.2008 08:02:45
Код является потокобезопасным по обычному определению. Вышеуказанная ситуация возникнет, только если вызывающая программа не является поточно-ориентированной.
James Anderson 12.12.2008 08:13:08

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

Главное, на что нужно обратить внимание - это статические переменные.

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

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

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

-4
12.12.2008 08:10:29
Arrgggh! Должно быть, "слепой код", это не потокобезопасность вообще! Сожалею.
James Anderson 12.12.2008 14:36:30

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

0
12.12.2008 08:11:02

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

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

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

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

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

3
12.12.2008 08:59:19

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

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

1
12.12.2008 09:04:23

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

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

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

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

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

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

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

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

4
12.12.2008 10:34:45
+1 за покрытие несвязных кэшей. Люди, говорящие о том, что потоки прерываются «между инструкциями ассемблера», полностью упускают этот момент, потому что если ваша операция выполнена полностью, это не означает, что другие потоки увидят результат сейчас, скоро или когда-либо ...
Steve Jessop 12.12.2008 12:40:34

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

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

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

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

0
12.12.2008 10:45:48
Звучит очень сексуально! Но зато в будущее :(
Meeh 12.12.2008 11:08:10
Я читал статью о программной реализации транзакционной памяти, как мне кажется, в Haskell, которая позволила компилятору обеспечить определенную степень безопасности (например, транзакционная функция не может вызвать необратимую). Очевидно, что аппаратная помощь может сделать такие вещи более производительными.
Steve Jessop 12.12.2008 12:43:37