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

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

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

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

Controls.CheckForIllegalCrossThread..blah =true

12 ответов

Решение

Eric Lippert есть хорошая запись в блоге под названием " Что это за вещь, которую вы называете" безопасная нить "? об определении безопасности потоков, найденном в Википедии.

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

"Часть кода является поточно-ориентированной, если она работает правильно при одновременном выполнении несколькими потоками".

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

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

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

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

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

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

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

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

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

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

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

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

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

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 возвращается. Теперь снова очередь за темой. Тема А продолжается. И добавляет 1 к tmp (tmp был 0 для потока A). И затем сохраняет это значение в myInt. myInt снова 1.

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

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

В реальном мире примером для неспециалистов является

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

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

Вы можете получить более подробное объяснение из книги "Параллелизм Java на практике":

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

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

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

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

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

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

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

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

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

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

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

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

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

См. Также пакеты java.util.concurrent и java.util.concurrent.atomic для других программных конструкций.

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

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

Я считаю, что концепция 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

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

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

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

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

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

Листинг 4.6. Класс неизменной точки, используемый 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) - недопустимое состояние. Таким образом, хотя базовые AtomicIntegers являются поточно-ориентированными, составной класс - нет. Потому что основные переменные состояния lower а также upper не являются независимыми, NumberRange не может просто делегировать безопасность потока его переменным состояния безопасности потока.

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

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

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

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