Что подразумевается под "потокобезопасным" кодом?

Означает ли это, что два потока не могут изменить базовые данные одновременно? Или это означает, что данный сегмент кода будет работать с предсказуемыми результатами, когда его выполняет более одного потока?

15 ответов

Решение

Из Википедии:

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

Есть несколько способов добиться безопасности потоков:

Re-entrancy:

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

Взаимное исключение:

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

Потоковое локальное хранилище:

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

Атомные операции:

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

прочитайте больше:

http://en.wikipedia.org/wiki/Thread_safety


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

http://mindprod.com/jgloss/threadsafe.html

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

totalRequests = totalRequests + 1
MOV EAX, [totalRequests]   // load memory for tot Requests into register
INC EAX                    // update register
MOV [totalRequests], EAX   // store updated value back to memory
  1. Первое условие состоит в том, что существуют области памяти, доступные из более чем одного потока. Как правило, эти местоположения являются глобальными / статическими переменными или являются доступными для динамической памяти из глобальных / статических переменных. Каждый поток получает свой собственный кадр стека для локальных переменных в области функций / методов, поэтому эти локальные переменные функции / метода otoh (которые находятся в стеке) доступны только из одного потока, которому принадлежит этот стек.
  2. Второе условие состоит в том, что существует свойство (часто называемое инвариантом), которое связано с этими расположениями общей памяти, которое должно быть истинным или допустимым, чтобы программа функционировала правильно. В приведенном выше примере свойство заключается в том, что "totalRequests должен точно представлять общее количество раз, когда какой-либо поток выполнял какую-либо часть инструкции приращения". Как правило, это неизменяемое свойство должно иметь значение true (в этом случае totalRequests должно содержать точное число), прежде чем произойдет обновление, чтобы обновление было корректным.
  3. Третье условие заключается в том, что свойство инварианта НЕ сохраняется во время какой-либо части фактического обновления. (Это временно недействительно или ложно во время некоторой части обработки). В этом конкретном случае с момента получения totalRequests до момента сохранения обновленного значения totalRequests не удовлетворяет инварианту.
  4. Четвертое и последнее условие, которое должно произойти для того, чтобы произошла гонка (и, следовательно, код НЕ должен быть "потокобезопасным"), заключается в том, что другой поток должен иметь возможность доступа к общей памяти, когда инвариант нарушен, что вызывает несогласованность или неправильное поведение

Мне нравится определение из параллелизма Java на практике Брайана Гетца за его полноту

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

Проще понять, что делает код не потокобезопасным. Есть две основные проблемы, которые заставят многопоточное приложение иметь нежелательное поведение.

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

  • Тупик, вызванный взаимной зависимостью от разделяемой переменной
    Если у вас есть две общие переменные A и B. В одной функции вы сначала блокируете A, а затем блокируете B. В другой функции вы начинаете блокировать B и через некоторое время блокируете A. Это потенциальный тупик, когда первая функция будет дождитесь разблокировки B, когда вторая функция будет ожидать разблокировки A. Эта проблема, вероятно, не будет возникать в вашей среде разработки и только время от времени. Чтобы избежать этого, все замки всегда должны быть в одном и том же порядке.

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

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

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

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

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

Вот почему некоторые люди предпочитают использовать термин внутренняя синхронизация.

Наборы терминологии

Есть три основных набора терминов для этих идей, с которыми я столкнулся. Первый и исторически более популярный (но еще хуже):

  1. потокобезопасный
  2. не потокобезопасный

Второй (и лучше):

  1. доказательство резьбы
  2. совместимость с резьбой
  3. нить враждебна

Третий:

  1. внутренне синхронизированный
  2. внешне синхронизированный
  3. несинхронизируемый

Аналогии

потокобезопасный ~ потокобезопасный ~ внутренне синхронизированный

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

не потокобезопасный (но приятный) ~ потоки совместимый ~ внешне синхронизированный ~ свободный поток

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

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

не потокобезопасный (и плохой) ~ поток враждебный ~ несинхронизируемый

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

Почему потокобезопасность и др. плохая терминология

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

ПРИМЕЧАНИЕ. Во многих руководствах по программному обеспечению фактически используется термин "потокобезопасность" для обозначения "потоковая совместимость", добавляя еще больше путаницы к тому, что уже было беспорядком! Я избегаю терминов "потокобезопасный" и "поток-небезопасный" любой ценой именно по этой причине, поскольку некоторые источники называют что-то "поточно-безопасным", а другие - "поточно-небезопасным", потому что они не могут согласиться от того, должны ли вы соответствовать некоторым дополнительным стандартам безопасности (примитивы синхронизации), или просто НЕ относиться враждебно, чтобы считаться "безопасным". Поэтому избегайте этих терминов и используйте вместо них более разумные термины, чтобы избежать опасного недопонимания с другими инженерами.

Напоминание о наших целях

По сути, наша цель - разрушить хаос.

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

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

Поток-безопасный код работает, как указано, даже если одновременно вводится разными потоками. Это часто означает, что внутренние структуры данных или операции, которые должны выполняться непрерывно, защищены от различных модификаций одновременно.

Ответим на это на примере:

class NonThreadSafe {

    private int counter = 0;

    public boolean countTo10() {
        count = count + 1;
        return (count == 10);
    }

В countTo10 Метод добавляет единицу к счетчику и затем возвращает истину, если счетчик достиг 10. Он должен возвращать истину только один раз.

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

Например, если count начинается с 9, один поток может добавить 1 к счету (что составляет 10), но затем второй поток может войти в метод и снова добавить 1 (что составляет 11) до того, как первый поток сможет выполнить сравнение с 10 Затем оба потока выполняют сравнение и обнаруживают, что count равно 11 и ни один из них не возвращает истину.

Таким образом, этот код не является потокобезопасным.

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

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

Проще говоря, код будет работать нормально, если многие потоки исполняют этот код одновременно.

Не путайте безопасность потоков с детерминизмом. Потокобезопасный код также может быть недетерминированным. Учитывая сложность отладки проблем с многопоточным кодом, это, вероятно, нормальный случай.:-)

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

В сущности, в многопоточной среде может произойти много ошибок (переупорядочивание инструкций, частично построенные объекты, одна и та же переменная, имеющая разные значения в разных потоках из-за кэширования на уровне ЦП и т. Д.).

Мне нравится определение, данное Java Concurrency на практике:

[Часть кода] является поточно-ориентированной, если она ведет себя корректно при доступе из нескольких потоков, независимо от планирования или чередования выполнения этих потоков средой выполнения, и без дополнительной синхронизации или другой координации со стороны код вызова.

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

Придуманный пример

Представьте, что вы реализуете счетчик. Вы можете сказать, что он ведет себя правильно, если:

  • counter.next() никогда не возвращает значение, которое уже было возвращено ранее (для простоты мы не предполагаем переполнения и т. д.)
  • все значения от 0 до текущего значения были возвращены на каком-то этапе (значение не пропускается)

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

Примечание: кросс-пост на программистов

И да и нет.

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

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

Я хотел бы добавить больше информации к другим хорошим ответам.

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

Посмотрите на этот вопрос SE для более подробной информации:

Что означает потокобезопасность?

Потокобезопасная программа гарантирует согласованность памяти.

Со страницы документации Oracle на расширенный параллельный API:

Свойства согласованности памяти:

Глава 17 Спецификации языка Java™ определяет отношение "до и после" для операций с памятью, таких как чтение и запись общих переменных. Результаты записи одним потоком гарантированно будут видимы для чтения другим потоком, только если операция записи происходит до операции чтения.

synchronized а также volatile конструкции, а также Thread.start() а также Thread.join() методы, могут образовывать отношения до и после.

Методы всех классов в java.util.concurrent и его подпакеты расширяют эти гарантии для синхронизации более высокого уровня. Особенно:

  1. Действия в потоке перед помещением объекта в любую параллельную коллекцию выполняются до выполнения действий после доступа или удаления этого элемента из коллекции в другом потоке.
  2. Действия в потоке до представления Runnable для Executor случиться - прежде чем начнется его исполнение. Аналогично для Callables, представленных ExecutorService,
  3. Действия, предпринятые асинхронным вычислением, представленным Future действия перед выполнением действия после получения результата через Future.get() в другой теме.
  4. Действия до "выпуска" методов синхронизатора, таких как Lock.unlock, Semaphore.release, and CountDownLatch.countDown действия, предшествующие успешному методу "получения", такие как Lock.lock, Semaphore.acquire, Condition.await, and CountDownLatch.await на том же объекте синхронизатора в другом потоке.
  5. Для каждой пары потоков, которые успешно обмениваются объектами через Exchanger, действия до exchange() в каждом потоке происходят - до тех, которые следуют за соответствующим exchange() в другом потоке.
  6. Действия до звонка CyclicBarrier.await а также Phaser.awaitAdvance (а также его варианты) действия, предшествующие действиям, выполняемым действием барьера, и действия, выполняемые действием барьера, действия, предшествующие успешному возврату из соответствующего await в других потоках.

Чтобы завершить другие ответы:

Синхронизация вызывает беспокойство только тогда, когда код в вашем методе выполняет одно из двух:

  1. работает с некоторым внешним ресурсом, который не является потокобезопасным.
  2. Читает или изменяет постоянный объект или поле класса

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

Планирование потоков не гарантируется для циклического перебора. Задача может полностью загружать процессор за счет потоков с одинаковым приоритетом. Вы можете использовать Thread.yield(), чтобы иметь совесть. Вы можете использовать (в Java) Thread.setPriority(Thread.NORM_PRIORITY-1), чтобы понизить приоритет потока

Плюс остерегайтесь:

  • большие затраты времени выполнения (уже упоминавшиеся другими) для приложений, которые выполняют итерации по этим "поточно-ориентированным" структурам.
  • Thread.sleep (5000) должен спать в течение 5 секунд. Однако, если кто-то изменит системное время, вы можете спать очень долго или вообще не спать. ОС записывает время пробуждения в абсолютной форме, а не в относительной.

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

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

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

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

Что еще более интересно, некоторые коллекции хеш-наборов, такие как исходная неуниверсальная коллекция в.NET, могут гарантировать, что до тех пор, пока ни один элемент не будет удален, и при условии, что только один поток когда-либо записывает в них, любой поток, который пытается чтение коллекции будет вести себя так, как если бы доступ к коллекции мог быть отложен и происходил в произвольном порядке, но в противном случае он будет вести себя нормально. Если поток №1 добавляет X, а затем Y, а поток №2 ищет и видит Y, а затем X, поток №2 может увидеть, что Y существует, а X нет; является ли такое поведение "потокобезопасным", будет зависеть от того, готов ли поток №2 справиться с такой возможностью.

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

Проще говоря:P Если безопасно выполнять несколько потоков в блоке кода, это потокобезопасно *

* применяются условия

Условия упоминаются другими ответами, такими как 1. Результат должен быть таким же, если вы выполняете один поток или несколько потоков над ним и т. Д.

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