Що таке умова гонки?


982

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

Мої запитання до громади:

Який стан перегонів?
Як ви їх виявляєте?
Як ти з ними поводишся?
Нарешті, як ви запобігаєте їх виникненню?


3
У розділі Безпечного програмування для Linux HOWTO є чудова глава, яка описує, що вони є, і як їх уникнути.
Крейг Н

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

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

Відповіді:


1237

Умова перегонів виникає, коли два або більше потоків можуть отримати доступ до спільних даних і вони намагаються змінити їх одночасно. Оскільки алгоритм планування потоків може змінюватися між потоками в будь-який час, ви не знаєте, в якому порядку потоки будуть намагатися отримати доступ до спільних даних. Отже, результат зміни даних залежить від алгоритму планування потоків, тобто обидва потоки "гоняться" для доступу / зміни даних.

Проблеми часто виникають, коли один потік робить "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.
}

Справа в тому, що 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

121
Що робить інша нитка, коли вона стикається з замком? Це чекає? Помилка?
Брайан Ортіз

173
Так, інший потік доведеться почекати, поки фіксатор буде відпущений, перш ніж він може продовжуватися. Це робить дуже важливим, щоб замок був звільнений утримуючим потоком, коли він закінчиться з ним. Якщо він ніколи не випускає його, то інший потік буде чекати нескінченно.
Лехане

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

2
@Despertar ... також не обов'язково так, що ресурси завжди потрібно ділити в системі, що має нитку. Наприклад, у вас може бути масив, де кожен елемент потребує обробки. Ви, можливо, можете розділити масив і мати нитку для кожного розділу, і потоки можуть виконувати свою роботу повністю незалежно один від одного.
Ян Варбуртон

12
Для перебігу змагань достатньо, щоб один потік намагався змінити спільні дані, тоді як решта потоків можуть або читати, або змінювати їх.
SomeWittyUsername

213

Існує "умова гонки", коли багатопотоковий (або інший паралельний) код, який має доступ до спільного ресурсу, може зробити це таким чином, щоб викликати несподівані результати.

Візьмемо цей приклад:

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

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

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

Отримайте значення x
Додайте 1 до цього значення
Збережіть це значення у x

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

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

Приклад:

Нитка 1: читає х, значення - 7
Нитка 1: додайте 1 до x, значення тепер 8
Нитка 2: читає х, значення - 7
Нитка 1: магазини 8 в х
Нитка 2: додає 1 до х, значення тепер 8
Нитка 2: магазини 8 в х

Умови перегонів можна уникнути, використовуючи якийсь механізм блокування перед кодом, який здійснює доступ до спільного ресурсу:

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

Тут відповідь виходить щоразу 50 000 000.

Щоб дізнатися більше про блокування, знайдіть: mutex, семафор, критичний розділ, спільний ресурс.


Див. Jakob.engbloms.se/archives/65 на прикладі програми для перевірки того, як такі речі йдуть погано ... це дійсно залежить від моделі пам'яті машини, на якій ви працюєте.
jakobengblom2

1
Як можна досягти 50 мільйонів, якщо він повинен зупинитися на 10 мільйонів?

9
@nocomprende: 5 потоків, які виконують той самий код за раз, як описано безпосередньо під фрагментом ...
Джон Скіт

4
@JonSkeet Ви праві, я переплутав i і x. Дякую.

Подвійне перевірка блокування в реалізації схеми Singleton є таким прикладом запобігання стану гонки.
Бхарат Додеджа

150

Що таке перегони?

Ви плануєте сходити в кіно о 17 годині вечора. Про наявність квитків ви дізнаєтесь о 16 годині вечора. Представник каже, що вони є в наявності. Ви розслабляєтесь і дістаєтесь до вікна квитка за 5 хвилин до виступу. Я впевнений, що ви можете здогадатися, що станеться: це повний зал. Проблема тут полягала в тривалості між перевіркою та дією. Ви поцікавилися в 4 та поступили в 5. Тим часом ще хтось схопив квитки. Це умова перегонів - конкретно сценарій "перевірити, а потім діяти".

Як ви їх виявляєте?

Релігійний огляд коду, багатопотокові тести на одиницю. Немає ярлика. На цьому з'явилося кілька плагінів Eclipse, але поки що нічого стабільного.

Як ви поводитесь і запобігаєте їм?

Найкраще було б створити вільні від побічних ефектів функції та без громадянства, максимально використовувати незмінні. Але це не завжди можливо. Тож використання java.util.concurrent.atomic, одночасних структур даних, належної синхронізації та одночасності на основі акторів допоможуть.

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


Огляди коду та тестові пристрої є вторинними для моделювання потоку між вашими вухами та меншого використання спільної пам'яті.
Acumenus

2
Я оцінив реальний приклад стану гонки
Том О.

11
Наче відповідь великі пальці вгору . Рішення полягає в тому, що ви заблокуєте квитки між 4-5 з mutex (взаємний виняток, c ++). У реальному світі це називається бронюванням квитків :)
Volt

1
було б гідною відповіддю, якщо ви скинете лише біти Java (питання не про Java, а про загальні умови гонки)
Corey Goldberg,

Ні. Це не умова гонки. З точки зору "бізнесу" ви просто чекали занадто довго. Очевидно, що замовлення не є рішенням. Спробуйте скальпер, інакше просто купуйте квиток як страхування
csherriff

65

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

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

Умова раси - це смислова помилка. Це вада, яка виникає в часі або впорядкованості подій, що призводить до помилкової поведінки програми .

Багато перегонів можуть бути (і насправді є) спричиненими перегонами даних, але це не обов'язково. Власне, перегони даних та умови перегонів не є ні необхідними, ні достатніми умовами один для одного. Ця публікація в блозі також дуже добре пояснює різницю, на прикладі простих банківських операцій. Ось ще один простий приклад, який пояснює різницю.

Тепер, коли ми прибили термінологію, спробуємо відповісти на початкове запитання.

Зважаючи на те, що умови перегонів - це смислові помилки, загального способу їх виявлення немає. Це пояснюється тим, що в загальному випадку немає можливості автоматичного оракула, який би міг відрізняти правильну та неправильну поведінку програми. Виявлення гонки - нерозв'язна проблема.

З іншого боку, перегони даних мають точне визначення, яке не обов'язково стосується правильності, і тому їх можна виявити. Існує багато варіантів детекторів перегонів даних (статичне / динамічне виявлення перегонів даних, виявлення перегонів на основі блокування даних, що відбувається раніше, ніж виявлення перегонів на основі даних, виявлення гібридних перегонів даних). Найсучаснішим динамічним детектором перегонів даних є ThreadSanitizer, який дуже добре працює на практиці.

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


Різниця є критичною для розуміння стану раси. Дякую!
ProgramCpp

37

Своєрідним канонічним визначенням є " коли два потоки отримують доступ до одного і того ж місця в пам'яті одночасно, і принаймні один з доступу є записом ". У ситуації "нитка" читача може отримати старе значення або нове значення, залежно від того, який потік "виграє гонку". Це не завжди помилка - насправді деякі дійсно волохаті низькі рівні алгоритми роблять це цілеспрямовано, але цього взагалі слід уникати. @Steve Gury - це хороший приклад того, коли це може бути проблемою.


3
Скажіть, будь ласка, приклад того, як умови гонки можуть бути корисними? Гуглінг не допоміг.
Олексій В.

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

33

Стан гонки - це певна помилка, яка трапляється лише за певних часових умов.

Приклад: Уявіть, що у вас є дві нитки - A і B.

У нитці A:

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

У нитці B:

object.a = 0

Якщо потік A попередньо перевіряється, перевіривши, що object.a не є нульовим, B зробить це a = 0, а коли потік A отримає процесор, він зробить "ділення на нуль".

Ця помилка трапляється лише тоді, коли потік A випущено відразу після оператора if, це дуже рідко, але це може статися.


21

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

За вікіпедією :

Термін походить від ідеї, що два сигнали перебігають один на одного, щоб впливати на вихід спочатку .

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

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

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

Вам потрібно зробити якусь заміну, щоб відобразити його у світі програмного забезпечення:

  • "два сигнали" => "два потоки" / "два процеси"
  • "впливати на вихід" => "впливати на деякий загальний стан"

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


20

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


просто геніальне пояснення
gokareless

Кінцевий стан чого?
Роман Олександрович

1
@ Роман Олександрович Остаточний стан програми. Стан, що стосується таких речей, як значення змінних тощо. Дивіться відмінну відповідь Лехана. "Стан" у його прикладі означатиме кінцеві значення "x" та "y".
AMTerp

19

Умови перегонів відбуваються у багатопотокових програмах або багатопроцесорних системах. В основному, умова гонки - це все, що робить припущення, що дві речі, що не знаходяться в одній нитці чи процесі, відбуватимуться в певному порядку, не вживаючи заходів для їх виконання. Це трапляється зазвичай, коли два потоки передають повідомлення шляхом встановлення та перевірки змінних членів класу, які обидва можуть отримати доступ. Майже завжди існує умова перегонів, коли одна нитка викликає сон, щоб дати іншій нитці час для виконання завдання (якщо тільки цей сон не знаходиться в циклі, з деяким механізмом перевірки).

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

Виявити умови гонки може бути важко, але є кілька ознак. Код, який сильно покладається на режим сну, схильний до умов перегонів, тому спочатку перевірте, чи є дзвінки спати в коді, що впливає. Додавання особливо довгого сну може також використовуватися для налагодження, щоб спробувати примусити певний порядок подій. Це може бути корисно для відтворення поведінки, бачачи, чи можете ви змусити її зникати, змінюючи терміни речей, і для тестування рішень, що застосовуються. Сну слід зняти після налагодження.

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


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 and 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.