Вопрос

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

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

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

Это было полезно?

Решение

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

Проблемы часто возникают, когда один поток выполняет команду «проверить-затем-действовать». (например, «проверить», если значение равно X, тогда «действовать», чтобы сделать что-то, что зависит от значения, являющегося 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.
}

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

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

// 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, он должен выполнить следующее:(очевидно, упрощенный)

Retrieve the value of x
Add 1 to this value
Store this value to x

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

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

Пример:

Thread 1: reads x, value is 7
Thread 1: add 1 to x, value is now 8
Thread 2: reads x, значение равно 7
Thread 1: stores 8 in x
Thread 2: adds 1 to x, value is now 8
Thread 2: хранит 8 в x

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

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. Вы также можете получить некоторые подробности о приведенном выше объяснении здесь .

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

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

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

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

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

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

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

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

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

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

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

В Потоке A:

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

В Потоке B:

object.a = 0

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

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

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

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

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

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

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

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

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

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

enter image description here

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

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

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

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

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

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

  

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

Что такое условие гонки?

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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();
        }
    }
}
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top