Как низко вы идете, прежде чем что-то становится потокобезопасным само по себе? - PullRequest
7 голосов
/ 12 декабря 2008

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

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

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

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

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

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

Ответы [ 10 ]

4 голосов
/ 12 декабря 2008

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

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

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

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

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

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

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

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

3 голосов
/ 12 декабря 2008

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

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

3 голосов
/ 12 декабря 2008

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

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

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

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

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

3 голосов
/ 12 декабря 2008

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

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

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

2 голосов
/ 12 декабря 2008

Это не поточно-ориентированный и не подходит для всех видов ситуаций.

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

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

2 голосов
/ 12 декабря 2008

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

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

1 голос
/ 12 декабря 2008

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

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

0 голосов
/ 12 декабря 2008

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

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

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

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

0 голосов
/ 12 декабря 2008

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

0 голосов
/ 12 декабря 2008

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

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

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

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

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

...