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


125

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

Но даже в этом случае я не совсем понимал, зачем нужен весь дополнительный код.

Обновление: столкнусь ли я с серьезными проблемами, если проверю

Controls.CheckForIllegalCrossThread..blah =true

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


@dave Извините, я попытался найти, но отказался ... все равно спасибо ..
Вивек Бернард

1
код, который не возникаетRace-Condition
Мухаммад Бабар

Ответы:


122

У Эрика Липперта есть хороший пост в блоге, озаглавленный « Что вы называете« потокобезопасным »? об определении безопасности потоков, найденном в Википедии.

3 важные вещи, извлеченные из ссылок:

«Часть кода является потокобезопасной, если она правильно функционирует при одновременном выполнении несколькими потоками».

«В частности, он должен удовлетворять потребность в нескольких потоках для доступа к одним и тем же общим данным…»

«… И необходимость того, чтобы общий фрагмент данных был доступен только одному потоку в любой момент времени».

Определенно стоит прочитать!


25
Пожалуйста, избегайте ответов только по ссылкам, так как в будущем они могут стать некорректными.
akhil_mittal


107

Проще говоря, потокобезопасность означает, что доступ к нему безопасен из нескольких потоков. Когда вы используете несколько потоков в программе, и каждый из них пытается получить доступ к общей структуре данных или месту в памяти, может произойти несколько плохих вещей. Итак, вы добавляете дополнительный код, чтобы предотвратить эти плохие вещи. Например, если два человека писали один и тот же документ в одно и то же время, второй человек, который должен сохранить, перезапишет работу первого человека. Чтобы сделать его потокобезопасным, вы должны заставить человека 2 ждать, пока человек 1 выполнит свою задачу, прежде чем позволить человеку 2 редактировать документ.


11
Это называется синхронизацией. Правильно?
JavaTechnical

3
Да. Заставить различные потоки ждать доступа к общему ресурсу можно с помощью синхронизации.
Винсент Рамдхани

Из принятого ответа Грегори он говорит: «Часть кода является потокобезопасной, если она правильно функционирует во время одновременного выполнения несколькими потоками». в то время как вы говорите: «Чтобы сделать это потокобезопасным, вы должны заставить человека 1 ждать»; разве он не говорит, что одновременная работа приемлема, в то время как вы говорите, что это не так? Не могли бы вы объяснить?
Дорогая,

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

Значит, это применимо только к коду, использующему глобальные и статические переменные? Используя ваш пример людей, редактирующих документы, я полагаю, что не имеет смысла запрещать человеку 2 запускать код написания документа в другом документе.
Аарон Франке

18

В Википедии есть статья о безопасности потоков.

Эта страница определений (вы должны пропустить рекламу - извините) определяет ее следующим образом:

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

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

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

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

  • Работа с копиями данных
  • Добавление блокировок вокруг критического кода

8

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

Рассмотрим следующий метод:

private int myInt = 0;
public int AddOne()
{
    int tmp = myInt;
    tmp = tmp + 1;
    myInt = tmp;
    return tmp;
}

Теперь поток A и поток B хотят выполнить AddOne (). но A запускается первым и считывает значение myInt (0) в tmp. Теперь по какой-то причине планировщик решает остановить поток A и отложить выполнение до потока B. Теперь поток B также считывает значение myInt (все еще 0) в свою собственную переменную tmp. Поток B завершает весь метод, поэтому в итоге myInt = 1. И возвращается 1. Теперь снова очередь потока А. Тема A продолжается. И добавляет 1 к tmp (tmp было 0 для потока A). А затем сохраняет это значение в myInt. myInt снова равен 1.

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

В нетривиальных случаях создавать потокобезопасные методы очень сложно. А техник довольно много. В Java вы можете пометить метод как синхронизированный, это означает, что только один поток может выполнять этот метод в данный момент. Остальные потоки ждут своей очереди. Это делает метод потокобезопасным, но если в методе необходимо выполнить много работы, это приводит к потере много места. Другой метод - «пометить только небольшую часть метода как синхронизированную».путем создания блокировки или семафора и блокировки этой небольшой части (обычно называемой критической частью). Существуют даже некоторые методы, которые реализованы как потокобезопасные без блокировки, что означает, что они построены таким образом, что несколько потоков могут проходить через них одновременно, не вызывая проблем, это может быть случай, когда метод только выполняет один атомарный вызов. Атомарные вызовы - это вызовы, которые нельзя прервать и которые могут выполняться только одним потоком за раз.


если метод AddOne вызывался два раза
Sujith PS

7

В реальном мире пример для непрофессионала

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

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


5

Вы можете получить более подробное объяснение из книги «Java Concurrency in Practice»:

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


4

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

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

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

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

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


3

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

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

Объекты блокировки поддерживают идиомы блокировки, которые упрощают работу многих параллельных приложений.

Исполнители определяют API высокого уровня для запуска и управления потоками. Реализации исполнителя, предоставляемые java.util.concurrent, обеспечивают управление пулом потоков, подходящее для крупномасштабных приложений.

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

У атомарных переменных есть функции, которые минимизируют синхронизацию и помогают избежать ошибок согласованности памяти.

ThreadLocalRandom (в JDK 7) обеспечивает эффективную генерацию псевдослучайных чисел из нескольких потоков.

Обратитесь к пакетам java.util.concurrent и java.util.concurrent.atomic, чтобы узнать о других конструкциях программирования.


1

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

Обычная практика WinForms - иметь один поток, посвященный всей вашей работе с пользовательским интерфейсом.


1

Я нахожу концепцию http://en.wikipedia.org/wiki/Reentrancy_%28computing%29 тем, что я обычно считаю небезопасной потоковой передачей, когда метод имеет побочный эффект, такой как глобальная переменная, и полагается на него.

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

//built in global set to locale specific value (here a comma)
decimalSeparator = ','

function FormatDot(value : real):
    //save the current decimal character
    temp = decimalSeparator

    //set the global value to be 
    decimalSeparator = '.'

    //format() uses decimalSeparator behind the scenes
    result = format(value)

    //Put the original value back
    decimalSeparator = temp

-2

Чтобы понять безопасность потоков, прочитайте разделы ниже :

4.3.1. Пример: средство отслеживания транспортных средств с использованием делегирования

В качестве более существенного примера делегирования давайте создадим версию трекера транспортных средств, который делегирует потокобезопасный класс. Мы храним место в карте, поэтому мы начнем с реализацией карт потокобезопасной, ConcurrentHashMap. Мы также сохраняем местоположение, используя неизменяемый класс Point вместо того MutablePoint, как показано в листинге 4.6.

Листинг 4.6. Неизменяемый класс Point, используемый DelegatingVehicleTracker.

 class Point{
  public final int x, y;

  public Point() {
        this.x=0; this.y=0;
    }

  public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

}

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

DelegatingVehicleTrackerв листинге 4.7 не используется явная синхронизация; весь доступ к состоянию управляется ConcurrentHashMap, и все ключи и значения карты неизменяемы.

Листинг 4.7. Делегирование безопасности потоков в ConcurrentHashMap.

  public class DelegatingVehicleTracker {

  private final ConcurrentMap<String, Point> locations;
    private final Map<String, Point> unmodifiableMap;

  public DelegatingVehicleTracker(Map<String, Point> points) {
        this.locations = new ConcurrentHashMap<String, Point>(points);
        this.unmodifiableMap = Collections.unmodifiableMap(locations);
    }

  public Map<String, Point> getLocations(){
        return this.unmodifiableMap; // User cannot update point(x,y) as Point is immutable
    }

  public Point getLocation(String id) {
        return locations.get(id);
    }

  public void setLocation(String id, int x, int y) {
        if(locations.replace(id, new Point(x, y)) == null) {
             throw new IllegalArgumentException("invalid vehicle name: " + id);
        }
    }

}

Если бы мы использовали исходный MutablePointкласс вместо Point, мы бы нарушили инкапсуляцию, позволив getLocationsопубликовать ссылку на изменяемое состояние, которое не является потокобезопасным. Обратите внимание, что мы немного изменили поведение класса трекера транспортных средств; в то время как версия монитора вернула снимок местоположений, версия с делегированием возвращает неизменяемое, но «живое» представление местоположений транспортных средств. Это означает, что если поток A вызывает, getLocationsа поток B позже изменяет расположение некоторых точек, эти изменения отражаются в карте, возвращаемой потоку A.

4.3.2. Независимые переменные состояния

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

VisualComponentв листинге 4.9 представлен графический компонент, который позволяет клиентам регистрировать слушателей для событий мыши и нажатия клавиш. Он поддерживает список зарегистрированных слушателей каждого типа, чтобы при возникновении события могли быть вызваны соответствующие слушатели. Но нет никакой связи между набором слушателей мыши и ключевыми слушателями; они независимы и поэтому VisualComponentмогут делегировать свои обязательства по безопасности потоков двум базовым спискам потоковой безопасности.

Листинг 4.9. Делегирование безопасности потоков для нескольких переменных базового состояния.

public class VisualComponent {
    private final List<KeyListener> keyListeners 
                                        = new CopyOnWriteArrayList<KeyListener>();
    private final List<MouseListener> mouseListeners 
                                        = new CopyOnWriteArrayList<MouseListener>();

  public void addKeyListener(KeyListener listener) {
        keyListeners.add(listener);
    }

  public void addMouseListener(MouseListener listener) {
        mouseListeners.add(listener);
    }

  public void removeKeyListener(KeyListener listener) {
        keyListeners.remove(listener);
    }

  public void removeMouseListener(MouseListener listener) {
        mouseListeners.remove(listener);
    }

}

VisualComponentиспользует CopyOnWriteArrayListдля хранения каждого списка слушателей; это поточно-ориентированная реализация List, особенно подходящая для управления списками слушателей (см. Раздел 5.2.3). Каждый список потокобезопасен, и потому , что нет никаких ограничений , связывающего состояние одного до состояния других, VisualComponentможет делегировать свои обязанности безопасности резьбы на основные mouseListenersи keyListenersобъекты.

4.3.3. Когда делегирование не удается

Большинство составных классов не так просты, как VisualComponent: у них есть инварианты, которые связывают переменные состояния их компонентов. NumberRangeв листинге 4.10 используется два AtomicIntegersдля управления своим состоянием, но налагается дополнительное ограничение - первое число должно быть меньше или равно второму.

Листинг 4.10. Класс диапазона чисел, который недостаточно защищает свои инварианты. Не делай этого.

public class NumberRange {

  // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

  public void setLower(int i) {
        //Warning - unsafe check-then-act
        if(i > upper.get()) {
            throw new IllegalArgumentException(
                    "Can't set lower to " + i + " > upper ");
        }
        lower.set(i);
    }

  public void setUpper(int i) {
        //Warning - unsafe check-then-act
        if(i < lower.get()) {
            throw new IllegalArgumentException(
                    "Can't set upper to " + i + " < lower ");
        }
        upper.set(i);
    }

  public boolean isInRange(int i){
        return (i >= lower.get() && i <= upper.get());
    }

}

NumberRangeне является потокобезопасным ; он не сохраняет инвариант, ограничивающий нижнее и верхнее. setLowerИ setUpperметоды пытаются соблюдать этот инвариант, но делает это плохо. Оба setLowerи setUpperпредставляют собой последовательности «проверка, затем действие», но они не используют достаточную блокировку, чтобы сделать их атомарными. Если диапазон номеров содержит (0, 10) и один поток вызывает, в setLower(5)то время как другой поток вызывает setUpper(4), с некоторым неудачным временем оба пройдут проверки в установщиках, и будут применены обе модификации. В результате диапазон теперь содержит (5, 4) - недопустимое состояние . Таким образом, в то время как базовые AtomicInteger являются потокобезопасными, составной класс - нет . Поскольку базовые переменные состояния lowerиupperне являются независимыми,NumberRange не может просто делегировать безопасность потоков своим переменным состояния безопасности потоков.

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.