Крім того, надмірне використання багатопоточності віднімає ресурси і час CPU на створення потоків і перемикання між потоками. Зокрема, коли використовуються операції чтения/записи на диск, швидшим може виявитися послідовне виконання завдань в одному або двох потоках, чим одночасне їх виконання в декількох потоках.
Створення|створіння| і запуск потоків С#
Для створення потоків використовується конструктор класу Thread, що приймає як параметр делегат типа ThreadStart, вказуючий метод, який потрібно виконати. Делегат ThreadStartвизначається так:
public delegate void ThreadStart();
Виклик методу Startпочинає виконання потоку. Потік триває до виходу з виконуваного методу. Ось приклад, що використовує повний синтаксис C# для створення делегата ThreadStart:
class| ThreadTest|
{
static| void| Main|()
{
Thread| t = new| Thread|(new| ThreadStart|(Go|));
t.Start(); // Виконати Go|() у новому потоці.
Go|(); // Одночасно запустити Go|() у головному|чільному| потоці.
}
static void Go() { Console.WriteLine("hello!"); }
В даному прикладі потік виконує метод Go() одночасно з головним потоком. Результат – два майже одночасних «hello»:
hello|! hello|!
У наступних|таких| таблицях приведена інформація про інструменти С# для координації (синхронізації) потоків:
Конструкція | Призначення |
Sleep | Блокування на вказаний час |
Join | Чекання закінчення іншого потоку |
Табл. 2.1 Прості методи блокування
Конструкція | Призначення | Доступна з інших процесів? | Швидкість |
Lock | Гарантує, що лише один потік може дістати доступ до ресурсу або секції коди. | немає | швидко |
Mutex | Гарантує, що лише один потік може дістати доступ до ресурсу або секції коди. Може використовуватися для запобігання запуску декількох екземплярів додатка. | так | середня |
Semaphore | Гарантує, що не більш заданого числа потоків може дістати доступ до ресурсу або секції коди. | так | середня |
Табл. 2.2 Блокувальні конструкції
Конструкція | Призначення | Доступна з інших процесів | Швидкість |
EventWaitHandle | Дозволяє потоку чекати сигналу від іншого потоку. | так | середньо |
Wait and Pulse | Дозволяє потоку чекати, поки не виконається задана умова блокування. | немає | середньо |
Табл. 2.3 Сигнальні конструкції
Конструкція | Призначення | Доступна з інших процесів | Швидкість |
Interlocked | Виконання простих не блокуючих атомарних операцій. | Так – через пам'ять, що розділяється | дуже швидко |
volatile | Для безпечного не блокуючого доступу до полів. | Так – через пам'ять, що розділяється | дуже швидко |
Табл. 2.4 Не блокуючі конструкції синхронізації
Блокування
Коли потік зупинений в результаті використання конструкцій, перерахованих в наведених вище таблицях, говорять, що він блокований. Будучи блокованим, потік негайно перестає отримувати час CPU, встановлює властивість ThreadStateв WaitSleepJoinі залишається в такому стані, поки не розблоковується. Розблокування може статися в наступних чотирьох випадках (кнопка виключення живлення не вважається!):
Потік не вважається блокованим, якщо його виконання припинене методом Suspend, що не рекомендується.
Виклик Thread.Sleep блокує поточний потік на вказаний час (або до переривання):
static| void| Main|()
{
Thread|.Sleep(0); |
Thread|.Sleep(1000);
Thread|.Sleep(TimeSpan|.FromHours(1));
Thread|.Sleep(Timeout|.Infinite);
}
Якщо бути точнішим, Thread.Sleepвідпускає CPU і повідомляє, що потоку не повинен виділятися час у вказаний період. Thread.Sleep(0) відпускає CPU для виділення одного кванта часу наступному потоку в черзі на виконання.
Унікальність Thread.Sleep серед інших методів блокування в тому, що він припиняє прокачування повідомлень Windows в додатках WindowsForms або COM-окружении потоку в однопоточному апартаменті. Через це тривале блокування головного (UI) потоку додатка WindowsForms наводить до того що додаток перестає відгукуватися – і отже, використання Thread.Sleep потрібно уникати незалежно від того, чи дійсно прокачування черги повідомлень технічно припинене. У старому COM-среде ситуація складніша, там інколи може бути бажане блокування за допомогою Sleepз одночасним прокачуванням черги повідомлень.
Клас Threadтакож надає метод SpinWait, який не відмовляється від часу CPU, а навпаки, завантажує процесор в циклі на задану кількість ітерацій. 50 ітерацій еквівалентні паузі приблизно в мікросекунду, хоча це залежить від швидкості і завантаження CPU. Технічно SpinWait– не блокуючий метод: ThreadStateтакого потоку не встановлюється в WaitSleepJoin, і потік не може бути перерваний з іншого потоку. SpinWaitрідко використовується – його головне вживання це чекання ресурсу, який повинен звільниться дуже скоро (у перебігу мікросекунд) без виклику Sleepі витрати процесорного часу на перемикання потоку. Проте ця методика вигідна лише на багатопроцесорних комп'ютерах, на однопроцесорному комп'ютері в ресурсу немає жодного шансу звільнитися, поки чекаючий на SpinWaitпотік не розтратить залишок кванта часу, а значить, необхідний результат недосяжний спочатку. А часті або тривалі виклики SpinWaitдаремно розтрачує час CPU.
Чекання|очікування| завершення потоку
Потік можна заблокувати до завершення іншого потоку викликом методу Join:
class| JoinDemo|
{
static| void| Main|()
{
Thread| t = new| Thread|(delegate|() { Console|.ReadLine(); });
t.Start();
t.Join(); // чекати, поки|доки| потік не завершиться
Console|.WriteLine("Thread| t's| ReadLine| complete|!");
}
}
Метод Joinможе також приймати як аргумент timeout- в мілісекундах або як TimeSpan. Якщо вказаний час витік, а потік не завершився, Joinповертає false. Joinз timeoutфункціонує як Sleep– фактично наступні два рядки коди наводять до однакового результату:
Thread|.Sleep(1000);
Thread.CurrentThread.Join(1000);
Блокування і потокова безпека
Блокування забезпечує монопольний доступ і використовується, аби|щоб| забезпечити виконання однієї секції коди лише|тільки| одним потоком одночасно. Для прикладу|зразка| розглянемо|розглядуватимемо| наступний|слідуючий| клас:
class| ThreadUnsafe|
{
static| int| val1|, val2|;
static| void| Go|()
{
if| (val2| != 0)
Console|.WriteLine(val1| / val2|);
val2| = 0;
}
}
Він не є потокобезпечним: якби метод Goвикликався двома потоками одночасно, можна було б отримати помилку ділення на 0, оскільки змінна val2 могла бути встановлена в 0 в одному потоці, у той час коли інший потік знаходився б між ifі Console.WriteLine.
От як за допомогою блокування можна вирішити цю проблему:
class| ThreadSafe|
{
static| object| locker| = new| object|();
static| int| val1|, val2|;
static| void| Go|()
{
lock| (locker|)
{
if| (val2| != 0)
Console|.WriteLine(val1| / val2|);
val2| = 0;
}
}
}
Лише один потік може одноразово заблокувати об'єкт синхронізації (в даному випадку locker), а всі інші конкуруючі потоки будуть припинені, поки блокування не буде знято. Якщо за блокування борються декілька потоків, вони ставляться в чергу чекання – "Readyqueue" – і обслуговуються, як тільки це стає можливим, за принципом “першим прийшов – першим обслужений”. Ексклюзивне блокування, як вже говорилося, забезпечує послідовний доступ до того, що вона захищає, так що виконувані потоки вже не можуть накластися один на одного. В даному випадку ми захистили логіку усередині методу Go, так само, як і поля val1 і val2.
Потік, заблокований на час чекання звільнення блокування, має властивість ThreadState, встановлену в WaitSleepJoin. Пізніше ми обговоримо, як потік, заблокований в такому стані, може бути примусово звільнений з іншого потоку викликом методів Interruptабо Abort. Це досить потужна можливість, використовувана зазвичай для завершення робочого потоку.
Оператор lockмови C# фактично є синтаксичним скороченням для викликів методів Monitor.Enter і Monitor.Exit в рамках блоків try-finally. Ось в що фактично розвертається реалізація методу Go з попереднього прикладу:
Monitor|.Enter(locker|);
try|
{
if| (val2| != 0)
Console|.WriteLine(val1| / val2|);
val2| = 0;
}
finally { Monitor.Exit(locker); }
Виклик Monitor.Exit без попереднього виклику Monitor.Enter для того ж об'єкту синхронізації викличе виключення.
Monitorтакож надає метод TryEnter, що дозволяє задати час чекання в мілісекундах або у вигляді TimeSpan. Метод повертає true, якщо блокування було отримане, і false, якщо блокування не було отримане за заданий час. TryEnterможе також бути викликаний без параметрів і в цьому випадку повертає управління негайно.
При неправильному використанні в блокування можуть бути і негативні наслідки – зменшення можливості паралельного виконання потоків, взаимоблокировки, гонки блокувань. Можливості для паралельного виконання зменшуються, коли надто багато коди поміщено в конструкцію lock, заставляючи інші потоки простоювати весь час, поки цей код виконується. Взаємоблокування настає, коли кожен з двох потоків чекає на блокуванні іншого потоку і, таким чином, ні той, ні інший не може рушити далі. Гонкою блокувань називається ситуація, коли будь-який з двох потоків може першим отримати блокування, проте програма ламається, якщо першим це зробить “неправильний” потік.