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


983

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

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

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


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

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

@MikeMB. Согласен, за исключением того, что, анализируя выполнение байт-кода, как это делает Race Catcher (см. Этот поток stackoverflow.com/a/29361427/1363844 ), мы можем обратиться ко всем тем примерно 62 языкам, которые компилируются в байт-код (см. En.wikipedia.org / wiki / List_of_JVM_languages )
Бен

Ответы:


1238

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

Проблемы часто возникают, когда один поток выполняет «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

121
Что делает другой поток, когда сталкивается с блокировкой? Это ждет? Ошибка?
Брайан Ортис

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

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

2
@Despertar ... Кроме того, это не обязательно так, что ресурсы всегда должны быть разделены в многопоточной системе. Например, у вас может быть массив, где каждый элемент нуждается в обработке. Вы можете разделить массив и иметь поток для каждого раздела, и потоки могут выполнять свою работу совершенно независимо друг от друга.
Ян Уорбертон

12
Чтобы произошла гонка, достаточно, чтобы один поток попытался изменить общие данные, тогда как остальные потоки могут либо прочитать, либо изменить их.
SomeWittyUsername

213

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

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

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 ответов.

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


См. Jakob.engbloms.se/archives/65, где приведен пример программы для проверки того, насколько часто такие вещи портятся ... это действительно зависит от модели памяти машины, на которой вы работаете.
jakobengblom2

1
Как он может достичь 50 миллионов, если он должен остановиться на 10 миллионов?

9
@nocomprende: 5 потоков, выполняющих один и тот же код за раз, как описано непосредственно под фрагментом ...
Джон Скит

4
@JonSkeet Вы правы, я перепутал i и x. Спасибо.

Двойная проверка блокировки в реализации шаблона Singleton является таким примером предотвращения состояния гонки.
Бхарат Додея

151

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

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

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

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

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

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

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


Обзоры кода и модульные тесты являются второстепенными по отношению к моделированию потока между вашими ушами и уменьшению использования общей памяти.
Acumenus

2
Я оценил реальный пример состояния гонки
Том О.

11
Как ответ палец вверх . Решение: вы блокируете тикеты между 4-5 с мьютексом (взаимное исключение, c ++). В реальном мире это называется резервированием билетов :)
Вольт

1
было бы неплохим ответом, если бы вы отбросили биты только для java (вопрос не о Java, а скорее об условиях гонки в целом)
Кори Голдберг,

Нет, это не состояние гонки. С точки зрения «бизнеса» вы просто слишком долго ждали. Очевидно, отставание не является решением. Попробуйте скальпер, иначе просто купите билет в качестве страховки
csherriff

65

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

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

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

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

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

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

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

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


Разница крайне важна для понимания состояния гонки. Спасибо!
ProgramCpp

37

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


3
Не могли бы вы привести пример того, как могут быть полезны условия гонки? Гугл не помог.
Алекс В.

3
@ Алекс V. На данный момент я понятия не имею, о чем я говорил. Я думаю, что это могло быть ссылкой на программирование без блокировок, но не совсем точно сказать, что это зависит от условий гонки как таковых.
Крис Конвей

34

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

Пример: представьте, что у вас есть две темы, 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, это очень редко, но это может произойти.


21

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

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

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

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

введите описание изображения здесь

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

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

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

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


20

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


просто блестящее объяснение
gokareless

Конечное состояние чего?
Роман Александрович

1
@RomanAlexandrovich Окончательное состояние программы. Состояние, относящееся к таким вещам, как значения переменных и т. Д. См. Отличный ответ Легана. «Состояние» в его примере будет относиться к конечным значениям «x» и «y».
AMTerp

19

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

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

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

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


10

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

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


5

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

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

Например, процессору A и процессору B необходимы идентичные ресурсы для их выполнения.

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

Есть инструменты для автоматического определения состояния гонки:

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

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

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

Существуют различные способы предотвращения состояния расы, такие как предотвращение критических участков .

  1. Нет двух процессов одновременно внутри их критических областей. ( Взаимное исключение)
  2. Не делается никаких предположений о скорости или количестве процессоров.
  3. Ни один процесс не работает за пределами своей критической области, которая блокирует другие процессы.
  4. Никакой процесс не должен ждать вечно, чтобы войти в его критическую область. (A ждет ресурсов B, B ждет ресурсов C, C ждет ресурсов A)

2

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

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


2

Вот классический пример сальдо банковского счета, который поможет новичкам понять потоки в 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));

}

1

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

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

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


0

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

    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;
        }
    }
}

0

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

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

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


На каком языке основан ваш ответ?
MikeMB

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

0

Рассмотрим операцию, которая должна отображать счет, как только счет увеличивается. т.е., как только 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 отобразит ее. Здесь существует условие гонки. Состояние гонки можно решить с помощью синхронизации


0

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

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