Підрозділ 15.2
Створення потоків. ThreadStart та ParameterizedThreadStart
15.2. Створення потоків. ThreadStart та ParameterizedThreadStart Будь який потік у .NET починається з делегата — посилання на метод, який потік виконуватиме. Конструктор класу Thread приймає делегат однієї з дв
15.2. Створення потоків. ThreadStart та ParameterizedThreadStart
Будь-який потік у .NET починається з делегата — посилання на метод, який потік виконуватиме. Конструктор класу Thread приймає делегат однієї з двох форм: ThreadStart або ParameterizedThreadStart. Розуміння відмінностей між ними та вміння обрати правильний підхід — основа грамотного використання багатопоточності.
Делегат ThreadStart — потік без параметрів
ThreadStart — найпростіша форма: делегат без параметрів і без значення, що повертається:
public delegate void ThreadStart();Він описує метод, який отримує все необхідне зі свого лексичного оточення (замикання, поля класу, константи) або взагалі не потребує зовнішніх даних. Саме цей делегат підходить для ізольованих завдань, де потік є самодостатнім.
Є три способи передати метод у конструктор Thread: іменований метод, анонімний метод та лямбда-вираз.
Іменований метод
Іменований метод — найзрозуміліший варіант: логіку видно у декларації методу, а не в місці створення потоку. Підходить для складних, об'ємних задач.
Анонімний метод
Анонімний метод зручний, коли логіка проста і тісно пов'язана з контекстом. Він автоматично захоплює змінні зовнішньої функції (замикання).
Лямбда-вираз
Лямбда-вираз є найпоширенішим сучасним підходом: компактна, читабельна, підтримує замикання. Для простих задач — оптимальний вибір.
Делегат ParameterizedThreadStart — потік з параметром
ParameterizedThreadStart відрізняється від ThreadStart одним параметром типу object?:
public delegate void ParameterizedThreadStart(object? obj);Відповідно, метод Thread.Start(object? parameter) приймає значення, яке буде передане у тіло потоку. Це дозволяє передавати дані у потік у момент його запуску, не вдаючись до замикань чи спільних полів:
Параметр завжди має тип object?, тому в тілі потоку його необхідно привести до потрібного типу. Якщо тип не збігається — as поверне null, що можна перевірити і обробити. Явне приведення через (T) кине InvalidCastException при невідповідності.
Передача кількох значень через клас-обгортку
Найсуттєве обмеження ParameterizedThreadStart — лише один параметр типу object?. Якщо потоку потрібно передати кілька значень, їх пакують у клас або структуру:
Використання is not PatientTask task — це патерн-матчинг: якщо param не є PatientTask, умова спрацьовує і метод завершується. Це безпечніше і лаконічніше за явне приведення та перевірку на null.
Замикання як альтернатива ParameterizedThreadStart
Коли потрібно передати дані в потік, замикання через лямбда-вираз часто зручніше за ParameterizedThreadStart. Лямбда захоплює змінні безпосередньо, без необхідності приведення типів:
Зверніть увагу на рядок string patient = patients[i]: ми копіюємо значення в окрему змінну перед тим, як лямбда її захопить. Це критично важливо. Якщо б лямбда захопила змінну i безпосередньо, всі потоки читали б одне й те саме значення i — те, яке воно буде в момент виконання лямбди, а не в момент її створення. Оскільки цикл завершується швидко, всі потоки могли б прочитати i == 3 і звернутись до одного й того ж елемента масиву (або вийти за межі). Копія виправляє цю проблему.
Порівняння підходів
Усі три підходи — іменований метод, ParameterizedThreadStart і лямбда з замиканням — вирішують одну задачу: зв'язати потік із кодом та даними. Вибір залежить від обсягу логіки та кількості параметрів:
| Підхід | Коли обирати |
|---|---|
Іменований метод + ThreadStart |
Велика, самодостатня логіка потоку, що варта окремого методу |
ParameterizedThreadStart |
Один або кілька параметрів, що передаються у момент Start() |
| Лямбда з замиканням | Коротка логіка, що використовує змінні навколишнього контексту |
Повний клінічний приклад: паралельна обробка черги
Цей приклад демонструє реальну паралельну обробку: чотири лабораторні замовлення виконуються одночасно у чотирьох незалежних потоках. Без багатопоточності загальний час склав би суму всіх затримок (1330 мс); з багатопоточністю — лише час найдовшої операції (450 мс). Метод Join() у фінальному циклі гарантує, що рядок «Всі аналізи оброблено» з'явиться тільки після завершення всіх чотирьох потоків.