OOP Course
Сьогодні

Підрозділ 15.Q

Питання для самоконтролю

Питання для самоконтролю — Розділ 15. Багатопоточність 15.1. Введення у багатопоточність. Клас Thread 1. Що таке процес і що таке потік? Яке ключове ресурсне розмежування між ними? Чому кілька потоків одного пр

Питання для самоконтролю — Розділ 15. Багатопоточність

15.1. Введення у багатопоточність. Клас Thread

  1. Що таке процес і що таке потік? Яке ключове ресурсне розмежування між ними? Чому кілька потоків одного процесу можуть ефективно обмінюватись даними, але це водночас породжує небезпеки?

  2. Поясніть, чому властивість Thread.Name можна присвоїти лише один раз. Яке виключення кинеться при повторному присвоєнні, і чому це обмеження зроблено навмисно?

  3. Що виведе наступний код — і чому результат може відрізнятися при кожному запуску?

    Thread t = new Thread(() => Console.WriteLine("Worker"));
    t.Start();
    Console.WriteLine("Main");
  4. Порівняйте передній (foreground) і фоновий (background) потоки. Напишіть приклад, де неправильний вибір типу потоку може призвести до незбереження важливих даних при завершенні програми.

  5. Метод Thread.Sleep(0) і Thread.Sleep(100) — у чому принципова різниця між ними? Коли доцільно використовувати Sleep(0)?

  6. Який стан набуває потік після виклику Join() на ньому іншим потоком? Що станеться, якщо викликати Join() на потоці, що вже завершився?

  7. Є код:

    Thread t = new Thread(() => { Thread.Sleep(5000); Console.WriteLine("Done"); });
    t.IsBackground = true;
    t.Start();
    Console.WriteLine("Main ends");

    Чи виведеться рядок "Done"? Обґрунтуйте відповідь.

  8. Спроєктуйте систему первинного прийому пацієнта, де паралельно виконуються: вимірювання показників (передній потік), завантаження медичної картки (передній потік) і архівація старих записів (фоновий потік). Поясніть, чому архівація є фоновим потоком, а не переднім.


15.2. Створення потоків. ThreadStart та ParameterizedThreadStart

  1. Що таке делегат ThreadStart? Як він відрізняється від ParameterizedThreadStart? Запишіть їх сигнатури.

  2. Знайдіть і виправте помилку в наступному коді:

    for (int i = 0; i < 3; i++)
    {
        new Thread(() => Console.WriteLine(i)).Start();
    }

    Що виведеться без виправлення і чому? Як правильно?

  3. Чому у ParameterizedThreadStart параметр має тип object? а не конкретний тип? Які два способи безпечно привести цей параметр до конкретного типу, і який з них кращий? Поясніть різницю між (T)param і param as T.

  4. Порівняйте три способи передачі коду у потік: іменований метод, анонімний метод і лямбда-вираз. Для якого сценарію ви б обрали кожен? Наведіть конкретний приклад.

  5. Напишіть код, що запускає 4 потоки паралельно — по одному на кожне лабораторне замовлення з масиву LabOrder[] — і чекає завершення всіх перед тим, як вивести «Всі аналізи готові».

  6. Що означає рядок string patient = patients[i] перед захопленням у лямбду в циклі? Що відбудеться, якщо замість копії захопити patients[i] напряму через i?

  7. Коли доцільно створювати клас-обгортку для передачі даних у потік через ParameterizedThreadStart? Наведіть приклад такого класу для клінічного сценарію.


15.3. Синхронізація потоків. Оператор lock та клас Lock

  1. Що таке стан перегонів (race condition)? Чому операція count++ є небезпечною при паралельному доступі, якщо виглядає як одна операція?

  2. Що виведе наступний код після завершення всіх потоків — і чи буде відповідь стабільною?

    int n = 0;
    Thread[] threads = new Thread[3];
    for (int i = 0; i < 3; i++)
        threads[i] = new Thread(() => { for (int j = 0; j < 1000; j++) n++; });
    foreach (var t in threads) t.Start();
    foreach (var t in threads) t.Join();
    Console.WriteLine(n);
  3. Чому не можна використовувати рядковий літерал або this як об'єкт-замок у lock? Яка стандартна практика для оголошення об'єкта-замка?

  4. Поясніть, що відбувається «під капотом», коли компілятор обробляє конструкцію lock (obj) { ... }. Напишіть еквівалентний код через Monitor.Enter / Monitor.Exit.

  5. Що таке взаємне блокування (deadlock)? Напишіть мінімальний приклад двох потоків, що гарантовано утворять дедлок, і поясніть, як його уникнути.

  6. Порівняйте volatile, Interlocked і lock: що гарантує кожен засіб і яка його вартість? Для яких конкретних сценаріїв підходить кожен?

  7. Клас Lock (.NET 9) надає метод EnterScope(). Чому патерн using (_lock.EnterScope()) надійніший за lock (_lock) { ... } з точки зору обробки виключень? Або вони еквівалентні — обґрунтуйте.

  8. Напишіть потокобезпечний клас BedManager, що управляє кількістю вільних ліжок у лікарні (операції Admit() і Discharge()), використовуючи lock для захисту спільного стану.


15.4. Клас Monitor

  1. У чому принципова різниця між Monitor і оператором lock? В яких трьох ситуаціях варто обирати Monitor замість lock?

  2. Поясніть три дії, що виконує Monitor.Wait(obj) атомарно. Чому умова перевіряється у циклі while, а не в if? Що таке «хибне пробудження» (spurious wakeup)?

  3. Чим відрізняється Monitor.Pulse(obj) від Monitor.PulseAll(obj)? В якому сценарії потрібен PulseAll? Наведіть клінічний приклад.

  4. Де в коді можна викликати Monitor.Wait(), Monitor.Pulse() і Monitor.PulseAll()? Яке виключення виникне при порушенні цієї вимоги?

  5. Знайдіть проблему в наступному коді:

    object _lock = new object();
    Queue<string> _queue = new Queue<string>();
    
    void Consumer()
    {
        lock (_lock)
        {
            if (_queue.Count == 0)
                Monitor.Wait(_lock);
            Console.WriteLine(_queue.Dequeue());
        }
    }

    Чому if тут небезпечний? Як виправити?

  6. Реалізуйте патерн «виробник — споживач» для лабораторної черги: медсестра додає зразки, лаборант забирає. Використовуйте Monitor.Wait і Monitor.Pulse. Забезпечте коректне завершення при порожній закритій черзі.

  7. Monitor.TryEnter(obj, 200) повернув false. Що це означає? Яку альтернативну логіку може виконати потік замість нескінченного очікування?


15.5. Клас AutoResetEvent

  1. Що таке AutoResetEvent? Яка ключова особливість відрізняє його від ManualResetEvent? Поясніть аналогію з турнікетом.

  2. Що відбудеться, якщо викликати Set() на AutoResetEvent, коли жоден потік не чекає на WaitOne()? Чи «накопичується» сигнал?

  3. Порівняйте AutoResetEvent з Monitor.Wait/Monitor.Pulse. Яка ключова відмінність у вимогах щодо спільного об'єкта-замка?

  4. Напишіть сценарій конвеєра із трьох кроків (реєстрація → огляд → аналізи), де кожен крок запускається в окремому потоці і чекає сигналу від попереднього через AutoResetEvent. Поясніть, чому всі потоки запускаються одночасно, але виконуються послідовно.

  5. Що повертає WaitOne(int milliseconds) при успішному і при тайм-аутовому завершенні? Напишіть приклад використання цього варіанту для обмеження часу очікування відповіді.

  6. Поясніть різницю між new AutoResetEvent(false) і new AutoResetEvent(true). В якому сценарії корисна початково сигнальна версія?

  7. Клінічна система чекає підтвердження від лаборатоії. Якщо підтвердження не надійде протягом 3 секунд — надіслати нагадування адміністратору. Реалізуйте цю логіку через WaitOne з тайм-аутом.


15.6. Клас Mutex

  1. Яка ключова властивість Mutex відрізняє його від lock? У яких двох конкретних сценаріях слід обирати Mutex?

  2. Що означає «реентерабельність» (recursive) Mutex? Що станеться, якщо потік, що тримає Mutex, викличе WaitOne() ще раз? І яке зобов'язання це накладає?

  3. Поясніть призначення параметра out bool createdNew у конструкторі іменованого Mutex. Як за допомогою цього параметра реалізувати «singleton-процес» (тільки один запущений екземпляр)?

  4. Що означає префікс Global\\ в імені іменованого Mutex? Чим він відрізняється від Local\\?

  5. Знайдіть потенційну проблему в наступному коді:

    Mutex m = new Mutex();
    m.WaitOne();
    DoWork(); // може кинути виняток
    m.ReleaseMutex();

    Як виправити, щоб Mutex завжди звільнявся?

  6. Що таке AbandonedMutexException? Коли він виникає і яке його практичне значення для відновлення після збою попереднього власника ресурсу?

  7. Порівняйте lock і Mutex за продуктивністю, областю дії та можливістю тайм-ауту. Склавши зведену таблицю, обґрунтуйте вибір для кожного з двох сценаріїв: (а) захист спільного лічильника в одному процесі; (б) заборона запуску другого екземпляра генератора звітів.


15.7. Клас Semaphore та SemaphoreSlim

  1. Що таке семафор? Поясніть аналогію зі стоянкою з N місцями. Чим він відрізняється від Mutex/lock?

  2. Порівняйте Semaphore і SemaphoreSlim: за продуктивністю, підтримкою async/await, можливістю іменування. Коли слід обирати кожен?

  3. Що означає new SemaphoreSlim(0, 10)? Що відбудеться з потоками, що викличуть Wait(), доки не буде викликано Release()?

  4. Напишіть код для системи, де 8 пацієнтів паралельно намагаються потрапити до 3 кабінетів УЗД. Забезпечте, щоб у будь-який момент в кабінетах перебувало не більше 3 пацієнтів. Розмістіть Release() у правильному місці.

  5. SemaphoreSlim.Release(5) — що означає передача числового аргументу? У якому сценарії (виробник пакетно публікує завдання) це корисно?

  6. Знайдіть проблему в коді:

    SemaphoreSlim sem = new SemaphoreSlim(3, 3);
    sem.Wait();
    ProcessData(); // може кинути виняток
    sem.Release();

    Як виправити?

  7. Спроєктуйте систему обмеження навантаження (rate limiting) для SMS-розсилки: шлюз дозволяє максимум 5 паралельних запитів. Реалізуйте через SemaphoreSlim, пояснивши логіку ініціалізації і де розміщується Wait()/Release().

  8. Склавши зведену таблицю всіх примітивів синхронізації розділу (lock, Monitor, AutoResetEvent, Mutex, Semaphore, SemaphoreSlim), відповідайте на три питання для кожного: скільки потоків пропускає одночасно, чи підтримує міжпроцесну синхронізацію, який основний сценарій використання.

Розроблено Tomka Yurii · © 2026 ·