Яка різниця між тупиком і лайлеком?


323

Чи може хто-небудь пояснити на прикладах (з коду), в чому різниця між тупиком та лайвелоком ?


Відповіді:


398

Взято з http://en.wikipedia.org/wiki/Deadlock :

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

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

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

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


8
Я знайшов це вже, але у них немає прикладів, як ви могли бачити, все одно спасибі
macindows

61
Я не наведу приклад коду, але розгляну два процеси, кожен з яких очікує ресурс, який має інший, але чекає не блокуючи. Коли кожен вчиться, вони не можуть продовжувати, вони випускають утримуваний ресурс і сплять протягом 30 секунд, потім вони витягують свій початковий ресурс з подальшим спробуванням ресурсу, який проводили інший процес, потім залишають і знову набувають. Оскільки обидва процеси намагаються впоратися (просто погано), це непросто.
ма

4
Ви можете навести мені той самий приклад, але з тупиком, дякую заздалегідь
macindows

32
Приклад тупикового кута набагато простіше ... припустимо два процеси A і B, і кожен хоче ресурсів r1 та ресурсу r2. Припустимо, A отримує (або вже має) r1, а B отримує (або вже має) r2. Тепер кожна спроба отримати ресурс, який має інший, без будь-якого тайм-ауту. A блокується тому, що B утримує r2, а B блокується, оскільки A утримує r1. Кожен процес заблокований і, таким чином, не може вивільнити ресурс, який хоче інший, що спричинить тупик.
ма

2
У контексті транзакційної пам’яті є чудове відео, де демонструється тупик та життєвий шлях: youtube.com/watch?v=_IxsOEEzf-c
BlackVegetable

78

Livelock

Нитка часто діє у відповідь на дію іншої нитки. Якщо дія іншої нитки також є відповіддю на дію іншої нитки, то може виникнути лайволк.

Як і у глухому куті, закручені нитки не в змозі досягти подальшого прогресу . Однак потоки не заблоковані - вони просто занадто зайняті реагуванням один на одного, щоб відновити роботу . Це можна порівняти з двома людьми, які намагаються пройти один одного в коридорі: Альфонс рухається ліворуч, щоб пропустити Гастона, а Гастон рухається праворуч, щоб Альфонс пройшов. Побачивши, що вони все ще блокують один одного, Альфонс рухається праворуч, а Гастон - ліворуч. Вони все ще блокують одне одного, і так далі ...

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

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

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



1
Ця річ має назву. Сленгове слово, можливо, але все-таки: schlumperdink : P
Джон Червоний

64

Весь зміст та приклади тут

Операційні системи: внутрішні принципи та принципи дизайну
William Stallings
8º Edition

Тупик : ситуація, коли два або більше процесів не в змозі протікати, тому що кожен чекає, коли інші щось робити.

Наприклад, розглянемо два процеси, P1 і P2, і два ресурси, R1 і R2. Припустимо, що кожен процес потребує доступу до обох ресурсів, щоб виконувати частину своєї функції. Тоді можлива така ситуація: ОС призначає R1 P2, а R2 P1. Кожен процес чекає одного з двох ресурсів. Жоден ресурс не звільнятиме ресурс, яким він уже володіє, поки не придбає інший ресурс і не виконає функцію, що вимагає обох ресурсів. Два процеси в тупику

Livelock : ситуація, коли два або більше процесів постійно змінюють свої стани у відповідь на зміни в інших процесах, не виконуючи жодної корисної роботи:

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

Припустимо, що три процеси (P1, P2, P3) потребують періодичного доступу до ресурсу Р. Розгляньте ситуацію, в якій P1 володіє ресурсом, і обидва P2 і P3 затримуються, чекаючи цього ресурсу. Коли P1 виходить з критичного розділу, P2 або P3 повинен бути дозволений доступ до R. Припустимо, що ОС надає доступ до P3 і що P1 знову вимагає доступу до того, як P3 завершить свій критичний розділ. Якщо ОС надає доступ до P1 після закінчення P3, а згодом по черзі надає доступ до P1 і P3, то P2 може бути безстроково відмовлено у доступі до ресурсу, навіть якщо ситуація з тупиком не існує.

ДОДАТОК А - ТЕМИ В СЛУЧАЙНІСТЬ

Приклад тупикового зв'язку

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

/* PROCESS 0 */
flag[0] = true;            // <- get lock 0
while (flag[1])            // <- is lock 1 free?
    /* do nothing */;      // <- no? so I wait 1 second, for example
                           // and test again.
                           // on more sophisticated setups we can ask
                           // to be woken when lock 1 is freed
/* critical section*/;     // <- do what we need (this will never happen)
flag[0] = false;           // <- releasing our lock

 /* PROCESS 1 */
flag[1] = true;
while (flag[0])
    /* do nothing */;
/* critical section*/;
flag[1] = false;

Приклад Livelock

/* PROCESS 0 */
flag[0] = true;          // <- get lock 0
while (flag[1]){         
    flag[0] = false;     // <- instead of sleeping, we do useless work
                         //    needed by the lock mechanism
    /*delay */;          // <- wait for a second
    flag[0] = true;      // <- and restart useless work again.
}
/*critical section*/;    // <- do what we need (this will never happen)
flag[0] = false; 

/* PROCESS 1 */
flag[1] = true;
while (flag[0]) {
    flag[1] = false;
    /*delay */;
    flag[1] = true;
}
/* critical section*/;
flag[1] = false;

[...] розглянемо таку послідовність подій:

  • P0 встановлює прапор [0] у значення true.
  • P1 встановлює прапор [1] у значення true.
  • P0 перевіряє прапор [1].
  • P1 перевіряє прапор [0].
  • P0 встановлює прапор [0] значення false.
  • P1 встановлює прапор [1] значення false.
  • P0 встановлює прапор [0] у значення true.
  • P1 встановлює прапор [1] у значення true.

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

Більше не вміст із книги.

А як щодо спинлок?

Spinlock - це методика уникнення витрат механізму блокування ОС. Зазвичай ви робите:

try
{
   lock = beginLock();
   doSomething();
}
finally
{
   endLock();
}

Проблема починає з’являтися, коли beginLock()коштує набагато більше doSomething(). У дуже перебільшеному вигляді уявіть, що відбувається, коли beginLockвитрачається 1 секунда, але doSomethingкоштує всього 1 мілісекунда.

У цьому випадку, якщо ви зачекали 1 мілісекунду, вам не вдасться перешкодити 1 секунду.

Чому beginLockб коштував стільки? Якщо замок вільний - це не багато коштує (див. Https://stackoverflow.com/a/49712993/5397116 ), але якщо замок не вільний, ОС "заморозить" вашу нитку, встановіть механізм, щоб вас розбудити коли замок буде звільнено, а потім знову розбудить вас.

Все це набагато дорожче, ніж деякі петлі, що перевіряють замок. Ось чому іноді краще робити «спінлок».

Наприклад:

void beginSpinLock(lock)
{
   if(lock) loopFor(1 milliseconds);
   else 
   {
     lock = true;
     return;
   }

   if(lock) loopFor(2 milliseconds);
   else 
   {
     lock = true;
     return;
   }

   // important is that the part above never 
   // cause the thread to sleep.
   // It is "burning" the time slice of this thread.
   // Hopefully for good.

   // some implementations fallback to OS lock mechanism
   // after a few tries
   if(lock) return beginLock(lock);
   else 
   {
     lock = true;
     return;
   }
}

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

Також дивіться:

https://preshing.com/20120226/roll-your-own-lightweight-mutex/
Чи правильна та оптимальна реалізація мого відключення спина?

Підсумок :

Тупик : ситуація, коли ніхто не просувається, нічого не робить (спить, чекає тощо). Використання процесора буде низьким;

Livelock : ситуація, коли ніхто не прогресує, але процесор витрачається на механізм блокування, а не на ваш розрахунок;

Голодування: ситуація, коли один прокурор ніколи не отримує шансу балотуватися; чистою невдачею чи якоюсь його властивістю (наприклад, низький пріоритет);

Спінлок : техніка уникнення вартості очікування звільнення замка.


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

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

Дякую за те, що ви додали про спінлок, на вашу думку, спінлок - це техніка, і ви також це виправдали, і я зрозумів. А як щодо тієї проблеми інверсії пріоритету, коли один процес P1 перебуває в критичному розділі, а інший процес з високим пріоритетом P2 отримує заплановану випередження P1, тоді в цьому випадку процесор знаходиться з P2, а наш механізм синхронізації - з P1. Це називається Spinlock, оскільки P1 знаходиться в стані готовності, а P2 - у стані запуску . Тут спінлок є проблемою. Я все виправдаю? Я не в змозі правильно зрозуміти тонкощі. Будь ласка, допоможіть
Vinay Yadav

Я пропоную вам створити ще одне питання, де чіткіше викладете вашу проблему. Тепер, якщо ви перебуваєте в "просторі користувача", а P1 знаходиться в критичному сеансі, захищеному SpinLock, реалізованим з нескінченним циклом та його попередньою можливістю; тоді P2 спробує ввести його, не вдасться і спалить увесь його часовий фрагмент. Ви створили мелодію (один процесор буде на 100%). (погано використовуватиметься захист IO синхронізації зі спінлок. Ви можете легко спробувати цей приклад) На "просторі ядра", можливо, ця примітка може вам допомогти: lxr.linux.no/linux+v3.6.6/Documentation/…
Даніель Фредеріко Лінс Лейт

Дуже дякую за роз’яснення. У будь-якому випадку, ваша відповідь була досить описовою та корисною на відміну від інших
Vinay Yadav

13

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

Умови LIVELOCK Livelock можуть виникати, коли від двох або більше завдань залежать і використовують деякий ресурс, спричиняючи умову кругової залежності, коли ці завдання продовжуються виконуватись назавжди, тим самим блокуючи виконання всіх завдань нижчого пріоритетного рівня (ці завдання з нижчим пріоритетом відчувають стан, який називають голодуванням)


Якщо "замикаються" завдання дотримуються протоколів арбітражу ресурсів, які включають затримки "backoff" і в результаті проводять більшу частину свого часу в стані сну, то інші завдання не будуть голодувати.
greggo

8

Можливо, ці два приклади ілюструють вам різницю між тупиком та рівнем життя:


Java-приклад для глухого кута:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockSample {

    private static final Lock lock1 = new ReentrantLock(true);
    private static final Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        Thread threadA = new Thread(DeadlockSample::doA,"Thread A");
        Thread threadB = new Thread(DeadlockSample::doB,"Thread B");
        threadA.start();
        threadB.start();
    }

    public static void doA() {
        System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
        lock1.lock();
        System.out.println(Thread.currentThread().getName() + " : holds lock 1");

        try {
            System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
            lock2.lock();
            System.out.println(Thread.currentThread().getName() + " : holds lock 2");

            try {
                System.out.println(Thread.currentThread().getName() + " : critical section of doA()");
            } finally {
                lock2.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
            }
        } finally {
            lock1.unlock();
            System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
        }
    }

    public static void doB() {
        System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
        lock2.lock();
        System.out.println(Thread.currentThread().getName() + " : holds lock 2");

        try {
            System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
            lock1.lock();
            System.out.println(Thread.currentThread().getName() + " : holds lock 1");

            try {
                System.out.println(Thread.currentThread().getName() + " : critical section of doB()");
            } finally {
                lock1.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
            }
        } finally {
            lock2.unlock();
            System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
        }
    }
}

Вибірка зразка:

Thread A : waits for lock 1
Thread B : waits for lock 2
Thread A : holds lock 1
Thread B : holds lock 2
Thread B : waits for lock 1
Thread A : waits for lock 2

Java-приклад для lvelock:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LivelockSample {

    private static final Lock lock1 = new ReentrantLock(true);
    private static final Lock lock2 = new ReentrantLock(true);

    public static void main(String[] args) {
        Thread threadA = new Thread(LivelockSample::doA, "Thread A");
        Thread threadB = new Thread(LivelockSample::doB, "Thread B");
        threadA.start();
        threadB.start();
    }

    public static void doA() {
        try {
            while (!lock1.tryLock()) {
                System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
                Thread.sleep(100);
            }
            System.out.println(Thread.currentThread().getName() + " : holds lock 1");

            try {
                while (!lock2.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
                    Thread.sleep(100);
                }
                System.out.println(Thread.currentThread().getName() + " : holds lock 2");

                try {
                    System.out.println(Thread.currentThread().getName() + " : critical section of doA()");
                } finally {
                    lock2.unlock();
                    System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
                }
            } finally {
                lock1.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
            }
        } catch (InterruptedException e) {
            // can be ignored here for this sample
        }
    }

    public static void doB() {
        try {
            while (!lock2.tryLock()) {
                System.out.println(Thread.currentThread().getName() + " : waits for lock 2");
                Thread.sleep(100);
            }
            System.out.println(Thread.currentThread().getName() + " : holds lock 2");

            try {
                while (!lock1.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + " : waits for lock 1");
                    Thread.sleep(100);
                }
                System.out.println(Thread.currentThread().getName() + " : holds lock 1");

                try {
                    System.out.println(Thread.currentThread().getName() + " : critical section of doB()");
                } finally {
                    lock1.unlock();
                    System.out.println(Thread.currentThread().getName() + " : does not hold lock 1 any longer");
                }
            } finally {
                lock2.unlock();
                System.out.println(Thread.currentThread().getName() + " : does not hold lock 2 any longer");
            }
        } catch (InterruptedException e) {
            // can be ignored here for this sample
        }
    }
}

Вибірка зразка:

Thread B : holds lock 2
Thread A : holds lock 1
Thread A : waits for lock 2
Thread B : waits for lock 1
Thread B : waits for lock 1
Thread A : waits for lock 2
Thread A : waits for lock 2
Thread B : waits for lock 1
Thread B : waits for lock 1
Thread A : waits for lock 2
Thread A : waits for lock 2
Thread B : waits for lock 1
...

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


Код приємний. Але приклад живого блокування - це не добре. Незалежно від того, чи блокується нитка за значенням або запитується на зміну значення, концептуально не відрізняється. Легка зміна, щоб краще проілюструвати блокування в реальному часі, полягає в тому, щоб потоки A і B звільнили блоки, які вони мають, коли вони зрозуміють, що не можуть отримати другий необхідний замок. Потім вони сплять кожну секунду, знову набувають замок, який вони спочатку мали, потім сплять ще одну секунду і намагаються знову придбати інший замок. Таким чином, цикл для кожного буде: 1) придбати-мій, 2) спати, 3) спробувати придбати інше & зазнати невдачі, 4) звільнити-міне, 5) спати, 6) повторити.
CognizantApe

1
Я сумніваюся, чи живі блоки, про які ви думаєте, дійсно існують досить довго, щоб вони створювали проблеми. Коли ви завжди відмовляєтесь від усіх заблокованих вами блокувань, коли не можете виділити наступний замок, умова "затримка і очікування" відсутня, оскільки насправді більше немає очікування. ( en.wikipedia.org/wiki/Deadlock )
mmirwaldt

насправді стан мертвого блокування відсутній, оскільки це - живі замки, про які ми говоримо. Приклад, який я наводив, схожий на стандартний приклад передпокою, наведений: geeksforgeeks.org/deadlock-starvation-and-livelock , en.wikibooks.org/wiki/Operating_System_Design/Concurrency/… , docs.oracle.com/javase/tutorial/essential / concurrency /…
CognizantApe

0

Уявіть, що ви маєте нитку A і потік B. Вони обидва synchronisedна одному об'єкті, і всередині цього блоку є глобальна змінна, яку вони обидва оновлюють;

static boolean commonVar = false;
Object lock = new Object;

...

void threadAMethod(){
    ...
    while(commonVar == false){
         synchornized(lock){
              ...
              commonVar = true
         }
    }
}

void threadBMethod(){
    ...
    while(commonVar == true){
         synchornized(lock){
              ...
              commonVar = false
         }
    }
}

Таким чином, коли потік А входить в whileпетлю і утримує блокування, він робить те , що він повинен робити , і встановити commonVarв true. Потім входить нитка B, входить у whileцикл і, оскільки commonVarє trueзараз, вона може утримувати замок. Це робиться так, виконує synchronisedблок і commonVarповертається до false. Тепер потік А знову отримує це нове вікно CPU, він був збирається вийти з whileциклу , але потік B тільки встановити його назад false, так що цикл повторюється знову. Нитки роблять щось (тому вони не заблоковані в традиційному розумінні), але майже нічого.

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


Гарний приклад. Це також ілюструє, чому ви завжди повинні читати та писати атомно у паралельному контексті. Якщо петлі в той час були всередині блоків синхронізації, то вищезгадане не буде проблемою.
CognizantApe
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.