JavaRush /Курси /C# SELF /Вступ до багатопоточності

Вступ до багатопоточності

C# SELF
Рівень 55 , Лекція 0
Відкрита

1. Вступ

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

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

Багатопоточність дає змогу застосунку робити кілька речей одночасно: інтерфейс лишається відгукливим, операції виконуються паралельно, і ми не зриваємося на монолог у стилі «Компʼютере, ти знову завис?!».

Базові поняття й термінологія

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

  • Процес (Process): Самостійна програма з власним адресним простором, змінними, ресурсами. Наприклад, кожен запущений застосунок у Windows — окремий процес.
  • Потік (Thread): Одиниця виконання всередині процесу. Процес може містити один або кілька потоків, що працюють з одними й тими самими ресурсами (памʼяттю, змінними).

Чому багатопоточність викликає стільки питань?

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

2. Історія та роль багатопоточності в C# і .NET

Раніше типові програми були простими й часто однопотоковими. Зі зростанням вимог до продуктивності, появою багатоядерних процесорів і потребою створювати відгукливі застосунки без зависань інтерфейсу у .NET зʼявилися засоби для багатопоточності. Спочатку це був класичний System.Threading.Thread, пізніше зʼявилися задачі (Task), асинхронні методи (async/await), паралельна обробка даних (PLINQ) і високорівневі примітиви синхронізації.

C# виріс у потужну платформу, де багатопоточність — не диковинка, а повсякденність.

Візуально: процес і потоки

Ось проста схема:


+--------------------------------------------------+
|                 Процес (твоя програма)           |
|      +-------------+   +-------------+           |
|      |    Потік 1  |   |   Потік 2   |           |
|      +-------------+   +-------------+           |
|                ...                                 |
|      +-------------+                              |
|      |    Потік N  |                              |
|      +-------------+                              |
+--------------------------------------------------+

Усі потоки всередині процесу бачать спільні змінні та ресурси.

3. Як створити потік у C#?

Почнемо з базового: класу Thread із простору імен System.Threading.

Приклад: запускаємо другий потік

Нехай у нас є тривале завдання — наприклад, підрахунок суми чисел від 1 до 10_000_000. Поки триває підрахунок, основний потік виводитиме привітання користувачеві.


using System;
using System.Threading;

class Program
{
    // Метод для другої задачі
    static void CalculateSum()
    {
        long sum = 0;
        for (int i = 1; i <= 10_000_000; i++)
            sum += i;
        Console.WriteLine($"[Потік 2] Сума: {sum}");
    }

    static void Main()
    {
        // Створюємо потік, вказуємо делегат на метод
        Thread thread = new Thread(CalculateSum);
        
        thread.Start(); // Запускаємо другий потік

        // Основний потік продовжує працювати
        Console.WriteLine("[Потік 1] Привіт! Працюємо паралельно...");

        // Чекаємо завершення другого потоку перед виходом
        thread.Join();

        Console.WriteLine("[Потік 1] Усе завершено!");
    }
}

Що відбудеться?
На екрані зʼявиться рядок "[Потік 1] Привіт! Працюємо паралельно...", а потім, коли потік із підрахунком завершить роботу, він виведе суму.

Типова проблема: хто перший, той і вивів

Спробуйте кілька разів запустити цей код — порядок виведення рядків може бути різним! Іноді сума зʼявляється першою, іноді — привітання. Оце й є справжня багатопоточність — програми стають менш передбачуваними, як настрій кота в понеділок.

4. Зона памʼяті: що бачать потоки?

Усі потоки всередині процесу мають доступ до тих самих змінних (якщо вони не локальні для методу). Якщо змінити змінну в одному потоці — її побачать інші!

Приклад: спільна змінна


using System;
using System.Threading;

class Program
{
    static int counter = 0;

    static void Increment()
    {
        for (int i = 0; i < 1000; i++)
            counter++;
    }

    static void Main()
    {
        Thread t1 = new Thread(Increment);
        Thread t2 = new Thread(Increment);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"counter = {counter}");
    }
}

Скільки очікуємо побачити в counter? Логіка підказує 2000, адже кожен потік збільшує по 1000 разів.

А от і ні! Запустіть кілька разів — побачите різні значення: 1 782, 1 935, 1 999…
Чому? Це класична проблема Race Condition — потоки «перехоплюють» одне в одного керування між читанням -> збільшенням -> записом, і частина інкрементів втрачається.

5. Корисні нюанси

Як потоки взаємодіють із інтерфейсом?

У сучасних настільних застосунках (WinForms/WPF/MAUI) основний потік обслуговує графічний інтерфейс користувача. Усі дії користувача (клацання, введення) — у цьому потоці. Фонові завдання мають виконуватися в інших потоках, але водночас за правилами не можна безпосередньо змінювати інтерфейс з інших потоків. Це зроблено, щоб уникнути хаосу.

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

Послідовність і паралельність

Невелика таблиця для закріплення різниці.

Однопотоковий код Багатопотоковий код
Виконує завдання по черзі Завдання можуть виконуватися одночасно
UI «зависає» під час довгих задач UI лишається відгукливим
Просто читати й писати змінні Потребує контролю доступу до даних
Налагодження просте Налагодження може бути складним

Важливі моменти під час роботи з потоками

  • Спільні змінні — спільний ризик. Як уже показано вище, якщо кілька потоків використовують спільну змінну — без синхронізації можливі помилки! (Детальніше — в наступних лекціях.)
  • Очікування потоку через Join(). Метод Join() дає змогу «призупинити» основний потік до завершення фонового. Використовуйте його, якщо треба дочекатися результату.
  • Потік не можна запустити двічі. Після виконання потоку його не можна знову запустити — доведеться створювати новий обʼєкт Thread.
  • Завершення потоку. Потік завершується, коли закінчується виконання його методу. Примусово «вбивати» потоки — погана ідея (метод Abort() давно вважається шкідливим і застарілим).

Для чого потрібна багатопоточність на практиці?

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

Проблеми багатопоточності

Багатопоточність дає потужність, але й додає складнощів:

  • Race Condition (стан гонки): коли кілька потоків одночасно змінюють одні й ті самі дані, результат залежить від порядку інструкцій, який непередбачуваний.
  • Deadlock (взаємне блокування): потоки чекають одне одного, і ніхто не може продовжити роботу.
  • Starvation (голодування): один із потоків постійно «обділений» доступом до ресурсу.

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

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ