JavaRush /Курси /C# SELF /Асинхронність vs. Багатопоточність (

Асинхронність vs. Багатопоточність ( async/await і Thread)

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

1. Вступ

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

Розберімося, у чому тут різниця.

Багатопоточність: коли працює багато рук

Багатопоточність — це організація роботи програми за допомогою кількох потоків. Потік — це нитка виконання, окрема «доріжка», якою процесор виконує інструкції програми. Один процес (наприклад, наш .NET-застосунок) може запустити водночас кілька потоків, щоб різні завдання виконувалися одночасно.

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

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

Асинхронність: уміння не простоювати дарма

Асинхронність — це організація коду так, щоб програма могла робити щось інше, поки очікує завершення тривалої операції (наприклад, відповіді з інтернету або читання файлу). Асинхронний код не обовʼязково використовує кілька потоків. Він просто не блокує виконання вашої програми на час очікування певної операції.

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

Ключова ідея асинхронності: не простоювати, поки очікуєте «довгі» завдання, а виконувати іншу корисну роботу.

2. А в чому різниця?

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

  • Багатопоточність потрібна, щоб реально паралелити роботу, використовуючи наявні процесори/ядра.
  • Асинхронність потрібна, щоб розумно організувати очікування ресурсів (мережеві I/O, дискові I/O тощо), не блокуючи потік.

Їх можна поєднувати, але вони не обовʼязково повʼязані.

Коротка візуалізація

Багатопоточність (Threads) Асинхронність (Async)
Коли застосовувати? Коли завдання «їсть» процесор (CPU-bound: обчислення, рендеринг, обробка масиву) Коли завдання «чекає» на подію (I/O-bound: мережа, диск, база даних)
Що робить код? Навантажує процесор, запускає кілька потоків, реально паралелить Коли очікує на дані («простоює»), звільняє потік — той може зайнятися чимось іншим
Чим керує? Кількістю одночасно виконуваних потоків Тим, які операції очікують, хто зайнятий/вільний і що виконати після завершення
Типовий приклад Стискання відео, рендеринг зображення Завантаження файлу, HTTP-запит до сервера

Приклад 1: Багатопоточність — рахуємо швидко

Припустімо, маємо складне завдання: порахувати суму великих чисел.


void ComputeSum(long start, long end)
{
    long sum = 0;
    for (long i = start; i <= end; i++)
    {
        sum += i;
    }
    Console.WriteLine($"Сума від {start} до {end} = {sum}");
}

// Запускаємо три завдання одночасно — кожне рахує свою частину роботи
Thread t1 = new Thread(() => ComputeSum(1, 1000_000_000));
Thread t2 = new Thread(() => ComputeSum(1000_000_001, 2000_000_000));
Thread t3 = new Thread(() => ComputeSum(2000_000_001, 3000_000_000));

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

// Дочікуємося завершення всіх потоків
t1.Join();
t2.Join();
t3.Join();

Навіщо тут потоки?
Бо це процесорне навантаження. Потоки справді завантажують процесор, і якщо у вас багатоядерна машина — робота прискориться.

Приклад 2: Асинхронність — чекаємо відповіді від сервера

Мережа — повільна річ, і поки ми чекаємо відповіді від сервера, потік може бути «вільним».


// Асинхронно завантажуємо сторінку сайту, потік не блокується
async Task DownloadPageAsync()
{
    using HttpClient client = new HttpClient();
    string html = await client.GetStringAsync("https://dotnet.microsoft.com/");
    Console.WriteLine(html.Length);
}

Навіщо тут асинхронність?
Ми даємо системі команду «Почни завантаження», і при цьому не блокуємо потік — очікуємо сповіщення, коли дані надійдуть.

3. Асинхронність без багатопоточності: міф чи правда?

Питання: Чи будь-який асинхронний код — це завжди запуск нового потоку?
Відповідь: Ні. Часто асинхронність узагалі не потребує додаткових потоків.

Коли ви, приміром, виконуєте await file.ReadAsync(...), .NET запускає операцію асинхронно на рівні ОС, і потік, що виконав цей виклик, одразу стає «вільним» і повертається в пул потоків. Коли операція завершиться, у пулі знайдеться будь-який вільний потік, який продовжить виконання вашого завдання.

  • Якби замість асинхронності ви використали звичайний (file.Read(...)) — потік просто очікував би завершення операції, нічого не роблячи.
  • Асинхронний код наче каже системі: «Поки чекаємо — займіться іншим!»

Важлива ілюстрація:


// Потік не блокується: очікуємо лише, доки операція завершиться
await Task.Delay(1000); // Просто зачекати секунду — не навантажує процесор!

Багатопоточність без асинхронності

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

Класика: обробка великих файлів


// Цей код справді завантажує процесор — асинхронність не допоможе.
void CalculateHash(string file)
{
    byte[] data = File.ReadAllBytes(file); // синхронно!
    // Рахуємо хеш...
}

Хочете пришвидшити — запускайте кілька потоків, кожен працює зі своїм файлом.

4. Як це виглядає у вашому застосунку?

Асинхронні операції (await)

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


async Task GetWeatherAsync(string city)
{
    using HttpClient client = new HttpClient();
    string json = await client.GetStringAsync($"https://api.weather.com/{city}");
    // Продовжуємо роботу, коли відповідь отримана
    Console.WriteLine($"Погода у {city}: {json}");
}

Що відбувається під капотом?

Виклик await «розрізає» ваш метод на дві частини:

  • Викликаємо асинхронну операцію — потік ОС «звільняється» і може виконувати інші завдання.
  • Коли дані отримано — виконання методу продовжується на одному з вільних потоків із пулу.

Багатопоточність для складних обчислень

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


// Ділимо велику задачу на невеликі частини, кожна рахує свою частину
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 4; i++)
{
    int rangeStart = i * 1000000;
    int rangeEnd = (i + 1) * 1000000 - 1;
    Thread t = new Thread(() => ComputeSum(rangeStart, rangeEnd));
    threads.Add(t);
    t.Start();
}
// Дочікуємося завершення всіх потоків
foreach (Thread t in threads) t.Join();

5. Типові помилки та нюанси під час роботи з асинхронністю

Помилка № 1: використання async «для швидкості».
Дуже поширене хибне уявлення — думати, що async пришвидшує виконання коду. Це не так. Асинхронність — це про відгукливість, а не про швидкість.

Якщо завдання CPU-bound (навантажує процесор) — асинхронність не зробить його швидшим.
Якщо завдання I/O-bound (робота з мережею, диском) — асинхронність корисна, бо потік не простоює дарма і може зайнятися іншою роботою.

Помилка № 2: блокування потоків через Wait() і Result.
В асинхронному коді не слід викликати Wait() або властивість Result на задачах. Це майже завжди призводить до блокувань потоків і взаємоблокувань.


// Погано! Заблокує потік і спричинить проблеми
var result = GetDataAsync().Result;

async Task<string> GetDataAsync() { /* ... */ return "data"; }

Правильний підхід — використовувати await і не блокувати потік.

Помилка № 3: асинхронність і UI.
У графічних застосунках (WPF, WinForms) головне — не «заморозити» UI-потік. Якщо в основному потоці запустити тривалу або блокувальну операцію, увесь застосунок «зависне». Асинхронність розв’язує цю проблему: важка робота виконується у фоновому режимі, а інтерфейс лишається відгукливим.

Помилка № 4: відсутність єдиної конвенції іменування асинхронних методів.
Якщо не додавати суфікс Async до асинхронних методів, легко заплутатися, який метод синхронний, а який — асинхронний. Це призводить до випадкових блокувань і помилок під час виклику. Завжди іменуйте асинхронні методи з Async у кінці.

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