Что такое состояние гонки?

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

Мои вопросы к сообществу:

Что такое состояние гонки? Как вы их обнаруживаете? Как вы справляетесь с ними? Наконец, как вы предотвращаете их возникновение?

20 ответов

Решение

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

Проблемы часто возникают, когда один поток выполняет "check-then-act" (например, "check", если значение равно X, затем "act", чтобы сделать что-то, зависящее от значения, являющегося X), а другой поток делает что-то со значением в между "чеком" и "актом". Например:

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"

   // If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
   // y will not be equal to 10.
}

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

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

// Obtain lock for x
if (x == 5)
{
   y = x * 2; // Now, nothing can change x until the lock is released. 
              // Therefore y = 10
}
// release lock for x

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

Возьмите этот пример:

for ( int i = 0; i < 10000000; i++ )
{
   x = x + 1; 
}

Если у вас было 5 потоков, выполняющих этот код одновременно, значение x НЕ БУДЕТ 50 000 000. Это на самом деле будет меняться с каждым прогоном.

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

Получить значение х
Добавьте 1 к этому значению
Сохранить это значение в х

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

Допустим, поток извлекает значение x, но еще не сохранил его. Другой поток может также получить то же значение x (потому что ни один поток еще не изменил его), и тогда они оба сохранят одно и то же значение (x+1) обратно в x!

Пример:

Поток 1: читает х, значение 7
Тема 1: добавьте 1 к x, значение теперь 8
Поток 2: читает х, значение 7
Тема 1: магазины 8 в х
Поток 2: добавляет 1 к x, значение теперь 8
Тема 2: магазины 8 в х

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

for ( int i = 0; i < 10000000; i++ )
{
   //lock x
   x = x + 1; 
   //unlock x
}

Здесь каждый раз получается 50 000 000 ответов.

Подробнее о блокировке ищите: мьютекс, семафор, критический раздел, общий ресурс.

Что такое состояние гонки?

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

Как вы их обнаруживаете?

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

Как вы справляетесь и предотвращаете их?

Лучше всего было бы создавать функции без побочных эффектов и без сохранения состояния, как можно больше использовать неизменяемые. Но это не всегда возможно. Так что использование java.util.concurrent.atomic, параллельные структуры данных, правильная синхронизация и параллелизм на основе акторов помогут.

Лучший ресурс для параллелизма - JCIP. Вы также можете получить более подробную информацию о приведенном выше объяснении здесь.

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

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

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

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

Теперь, когда мы прибегли к терминологии, давайте попробуем ответить на первоначальный вопрос.

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

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

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

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

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

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

Пример: представьте, что у вас есть две темы, A и B.

В теме А:

if( object.a != 0 )
    object.avg = total / object.a

В теме B:

object.a = 0

Если поток A выгружается сразу после проверки того, что object.a не равен NULL, B сделает a = 0, и когда поток A получит процессор, он выполнит "деление на ноль".

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

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

Согласно википедии:

Термин происходит от идеи, что два сигнала мчатся друг с другом, чтобы сначала повлиять на выход.

Состояние гонки в логической схеме:

Индустрия программного обеспечения приняла этот термин без изменений, что делает его немного сложным для понимания.

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

  • "два сигнала" => "два потока"/"два процесса"
  • "влиять на вывод" => "влиять на общее состояние"

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

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

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

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

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

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

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

Хорошо, вот 4 вопроса. один за другим ответ как под....

Что такое состояние гонки?

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

Как вы их обнаруживаете?

Это приводит к ошибке, которую трудно локализовать.

Как вы справляетесь с ними?

Использовать семафоры

И наконец,

Как вы предотвращаете их появление?

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

Вы можете предотвратить состояние гонки, если вы используете "Атомные" классы. Причина в том, что поток не разделяет операцию get и set, пример ниже:

AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);

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

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

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

Я сделал видео, которое объясняет это.

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

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

Вы можете просмотреть его здесь. https://youtu.be/RWRicNoWKOY

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

public class BankAccount {

/**
 * @param args
 */
int accountNumber;
double accountBalance;

public synchronized boolean Deposit(double amount){
    double newAccountBalance=0;
    if(amount<=0){
        return false;
    }
    else {
        newAccountBalance = accountBalance+amount;
        accountBalance=newAccountBalance;
        return true;
    }

}
public synchronized boolean Withdraw(double amount){
    double newAccountBalance=0;
    if(amount>accountBalance){
        return false;
    }
    else{
        newAccountBalance = accountBalance-amount;
        accountBalance=newAccountBalance;
        return true;
    }
}

public static void main(String[] args) {
    // TODO Auto-generated method stub
    BankAccount b = new BankAccount();
    b.accountBalance=2000;
    System.out.println(b.Withdraw(3000));

}

Рассмотрим операцию, которая должна отображать счет, как только счет увеличивается. т.е., как только CounterThread увеличивает значение, DisplayThread должен отобразить недавно обновленное значение.

int i = 0;

Выход

CounterThread -> i = 1  
DisplayThread -> i = 1  
CounterThread -> i = 2  
CounterThread -> i = 3  
CounterThread -> i = 4  
DisplayThread -> i = 4

Здесь CounterThread часто получает блокировку и обновляет значение, прежде чем DisplayThread отобразит ее. Здесь существует условие гонки. Состояние гонки можно решить с помощью синхронизации

public class Synchronized_RACECONDITION {
    private static final int NUM_INCREMENTS = 10000;

    private static int count = 0;

    public static void main(String[] args) {
        testSyncIncrement();
        testNonSyncIncrement();
    }

    private static void testSyncIncrement() {
        count = 0;

        ExecutorService executor = Executors.newFixedThreadPool(2);

        IntStream.range(0, NUM_INCREMENTS)
                .forEach(i -> executor.submit(Synchronized_RACECONDITION::incrementSync));

        ConcurrentUtils.stop(executor);

        System.out.println("   Sync: " + count);
    }

    private static void testNonSyncIncrement() {
        count = 0;

        ExecutorService executor = Executors.newFixedThreadPool(2);

        IntStream.range(0, NUM_INCREMENTS)
                .forEach(i -> executor.submit(Synchronized_RACECONDITION::increment));

        ConcurrentUtils.stop(executor);

        System.out.println("NonSync: " + count);
    }

    private static synchronized void incrementSync() {
        count = count + 1;
    }

    private static void increment() {
        count = count + 1;
    }
static  class ConcurrentUtils {

    public static void stop(ExecutorService executor) {
        try {
            executor.shutdown();
            executor.awaitTermination(60, TimeUnit.SECONDS);
        }
        catch (InterruptedException e) {
            System.err.println("termination interrupted");
        }
        finally {
            if (!executor.isTerminated()) {
                System.err.println("killing non-finished tasks");
            }
            executor.shutdownNow();
        }
    }
}
}

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

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

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

Более подробную информацию о состоянии гонки можно получить здесь, http://msdn.microsoft.com/en-us/magazine/cc546569.aspx.

Попробуйте этот базовый пример для лучшего понимания состояния гонки:

    public class ThreadRaceCondition {

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Account myAccount = new Account(22222222);

        // Expected deposit: 250
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.DEPOSIT, 5.00);
            t.start();
        }

        // Expected withdrawal: 50
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.WITHDRAW, 1.00);
            t.start();

        }

        // Temporary sleep to ensure all threads are completed. Don't use in
        // realworld :-)
        Thread.sleep(1000);
        // Expected account balance is 200
        System.out.println("Final Account Balance: "
                + myAccount.getAccountBalance());

    }

}

class Transaction extends Thread {

    public static enum TransactionType {
        DEPOSIT(1), WITHDRAW(2);

        private int value;

        private TransactionType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    };

    private TransactionType transactionType;
    private Account account;
    private double amount;

    /*
     * If transactionType == 1, deposit else if transactionType == 2 withdraw
     */
    public Transaction(Account account, TransactionType transactionType,
            double amount) {
        this.transactionType = transactionType;
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        switch (this.transactionType) {
        case DEPOSIT:
            deposit();
            printBalance();
            break;
        case WITHDRAW:
            withdraw();
            printBalance();
            break;
        default:
            System.out.println("NOT A VALID TRANSACTION");
        }
        ;
    }

    public void deposit() {
        this.account.deposit(this.amount);
    }

    public void withdraw() {
        this.account.withdraw(amount);
    }

    public void printBalance() {
        System.out.println(Thread.currentThread().getName()
                + " : TransactionType: " + this.transactionType + ", Amount: "
                + this.amount);
        System.out.println("Account Balance: "
                + this.account.getAccountBalance());
    }
}

class Account {
    private int accountNumber;
    private double accountBalance;

    public int getAccountNumber() {
        return accountNumber;
    }

    public double getAccountBalance() {
        return accountBalance;
    }

    public Account(int accountNumber) {
        this.accountNumber = accountNumber;
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean deposit(double amount) {
        if (amount < 0) {
            return false;
        } else {
            accountBalance = accountBalance + amount;
            return true;
        }
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean withdraw(double amount) {
        if (amount > accountBalance) {
            return false;
        } else {
            accountBalance = accountBalance - amount;
            return true;
        }
    }
}
Другие вопросы по тегам