Робота з потоками в delphi так чи страшний чорт, як його малюють програмні продукти

Нерідко зустрічав на форумах думки, що потоки не потрібні взагалі, будь-яку програму можна написати так, що вона буде чудово працювати і без них. Звичайно, якщо не робити нічого серйозніше "Hello World" це так і є, але якщо поступово набирати досвід, рано чи пізно будь-який програміст упреться в можливості "плоского" коду, виникне необхідність распараллелить завдання. А деякі завдання взагалі не можна реалізувати без використання потоків, наприклад робота з сокетами, COM-портом, тривале очікування будь-яких подій, і т.д.

Всім відомо, що Windows система багатозадачна. Попросту кажучи, це означає, що кілька програм можуть працювати одночасно під управлінням ОС. Всі ми відкривали диспетчер задач і бачили список процесів. Процес - це екземпляр виконуваного застосування. Насправді сам по собі він нічого не виконує, він створюється під час запуску програми, містить в собі службову інформацію, через яку система з ним працює, так само йому виділяється необхідна пам'ять під код і дані. Для того, щоб програма запрацювала, в ньому створюється потік. Будь-який процес містить в собі хоча б один потік, і саме він відповідає за виконання коду і отримує на це процесорний час. Цим і досягається уявна паралельність роботи програм, або, як її ще називають, псевдопараллельность. Чому уявна? Та тому, що реально процесор в кожен момент часу може виконувати тільки одну ділянку коду. Windows роздає процесорний час всім потокам в системі по черзі, тим самим створюється враження, що вони працюють одночасно. Реально працюють паралельно потоки можуть бути тільки на машинах з двома і більше процесорами.

Для створення додаткових потоків в Delphi існує базовий клас TThread. від нього ми і будемо успадковуватися при реалізації своїх потоків. Для того, щоб створити "скелет" нового класу, можна вибрати в меню File - New - Thread Object, Delphi створить новий модуль з заготівлею цього класу. Я ж для наочності опишу його в модулі форми. Як бачите, в цій заготівлі доданий один метод - Execute. Саме його нам і потрібно перевизначити, код всередині нього і буде працювати в окремому потоці. І так, спробуємо написати приклад - запустимо в потоці нескінченний цикл:

Запустіть приклад на виконання і натисніть кнопку. Начебто нічого не відбувається - форма не зависла, реагує на переміщення. Насправді це не так - відкрийте диспетчер задач і ви побачите, що процесор завантажений по-повній. Зараз в процесі вашої програми працює два потоки - один був створений спочатку, під час запуску програми. Другий, який так вантажить процесор - ми створили після натискання кнопки. Отже, давайте розберемо, що ж означає код в Button1Click:

NewThread: = TNewThread.Create (true); тут ми створили екземпляр класу TNewThread. Конструктор Create має всього один параметр - CreateSuspended типу boolean, який вказує, запустити новий потік відразу після створення (якщо false), або дочекатися команди (якщо true). New.FreeOnTerminate: = true; властивість FreeOnTerminate визначає, що потік після виконання автоматично завершиться, об'єкт буде знищений, і нам не доведеться його знищувати вручну. У нашому прикладі це не має значення, так як сам по собі він ніколи не завершиться, але знадобиться в наступних прикладах. NewThread.Priority: = tpLower; Властивість Priority, якщо ви ще не здогадалися з назви, встановлює пріоритет потоку. Да да, кожен потік в системі має свій пріоритет. Якщо процесорного часу не вистачає, система починає розподіляти його відповідно до пріоритетів потоків. Властивість Priority може набувати таких значень:
  • tpTimeCritical - критичний
  • tpHighest - дуже високий
  • tpHigher - високий
  • tpNormal - середній
  • tpLower - низький
  • tpLowest - дуже низький
  • tpIdle - потік працює під час простою системи
Ставити високі пріоритети потокам не варто, якщо цього не вимагає завдання, так як це сильно навантажує систему. NewThread.Resume; Ну і власне, запуск потоку.

Думаю, тепер вам зрозуміло, як створюються потоки. Зауважте, нічого складного. Але не все так просто. Здавалося б - пишемо будь-який код всередині методу Execute і все, а ні, потоки мають одну неприємну властивість - вони нічого не знають один про одного. І що такого? - запитаєте ви. А ось що: припустимо, ви намагаєтеся з іншого потоку змінити властивість якогось компонента на формі. Як відомо, VCL однопоточні, весь код всередині програми виконується послідовно. Припустимо, в процесі роботи змінилися якісь дані всередині класів VCL, система відбирає час у основного потоку, передає по колу іншим потокам і повертає назад, при цьому виконання коду триває з того місця, де призупинилося. Якщо ми зі свого потоку щось змінюємо, наприклад, на формі, задіюється багато механізмів всередині VCL (нагадаю, виконання основного потоку поки "призупинено"), відповідно за цей час встигнуть змінитися будь-які дані. І тут раптом час знову віддається основному потоку, він спокійно продовжує своє виконання, але дані вже змінені! До чого це може призвести - передбачити не можна. Ви можете перевірити це тисячу разів, і нічого не станеться, а на тисячу перший програма впаде. І це відноситься не тільки до взаємодії додаткових потоків з головним, але і до взаємодії потоків між собою. Писати такі ненадійні програми звичайно не можна.

Ось ми і підійшли до дуже важливого питання - синхронізації потоків.

Ось тепер ProgressBar рухається, і це цілком безпечно. А безпечно ось чому: процедура Synchronize на час призупиняє виконання нашого потоку, і передає управління головного потоку, тобто SetProgress виконується в головному потоці. Це потрібно запам'ятати, тому що деякі допускають помилки, виконуючи всередині Synchronize тривалу роботу, при цьому, що очевидно, форма зависає на тривалий час. Тому використовуйте Synchronize для виведення інформації - то саме Пересування прогресу, оновлення заголовків компонентів і т.д.

Ви напевно помітили, що всередині циклу ми використовуємо процедуру Sleep. У однопоточном додатку Sleep використовується рідко, а ось в потоках його використовувати дуже зручно. Приклад - нескінченний цикл, поки не виконається яке-небудь умова. Якщо не вставити туди Sleep ми будемо просто навантажувати систему марною роботою.

Сподіваюся, ви зрозуміли як працює Synchronize. Але є ще один досить зручний спосіб передати інформацію формі - посилка повідомлення. Давайте розглянемо і його. Для цього оголосимо константу:

Тепер ми трохи змінимо, можна сказати навіть спростимо, реалізацію методу Execute нашого потоку:

Використовуючи функцію SendMessage, ми посилаємо вікна додатка повідомлення, один з параметрів якого містить потрібний нам прогрес. Повідомлення стає в чергу, і згідно цієї черги буде оброблено головним потоком, де і виконається метод SetProgressPos. Але тут є один нюанс: SendMessage, як і в випадку з Synchronize, призупинить виконання нашого потоку, поки основний потік не обробить повідомлення. Якщо використовувати PostMessage цього не відбудеться, наш потік відправить повідомлення і продовжить свою роботу, а вже коли воно там буде опрацьовано - неважливо. Яку з цих функцій використовувати - вирішувати вам, все залежить від завдання.

Ось, в принципі, ми і розглянули основні способи роботи з компонентами VCL з потоків. А як бути, якщо в нашій програмі не один новий потік, а кілька? І потрібно організувати роботу з одними і тими ж даними? Тут нам на допомогу приходять інші способи синхронізації. Один з них ми і розглянемо. Для його реалізації потрібно додати в проект модуль SyncObjs.

Найцікавіший спосіб, на мій погляд - критичні секції

Працюють вони в такий спосіб: всередині критичної секції може працювати тільки один потік, інші чекають його завершення. Щоб краще зрозуміти, всюди призводять порівняння з вузькою трубою: уявіть, з одного боку "товпляться" потоки, але в трубу може "пролізти" тільки один, а коли він "пролізе" - почне рух другої, і так по порядку. Ще простіше зрозуміти це на прикладі і тим же ProgressBar'ом. Отже, запустіть один із прикладів, наведених раніше. Натисніть на кнопку, зачекайте кілька секунд, а потім натисніть ще раз. Що відбувається? ProgressBar почав стрибати. Стрибає тому, що у нас працює не один потік, а два, і кожен з них передає різні значення прогресу. Тепер трохи переробимо код, в подію onCreate форми створимо критичну секцію:

У TCriticalSection є два потрібних нам методу, Enter і Leave, відповідно вхід і вихід з неї. Помістимо наш код в критичну секцію:

Спробуйте запустити додаток і натиснути кілька разів на кнопку, а потім посНовомосковскйте, скільки разів пройде прогрес. Зрозуміло, в чому суть? Перший раз, натискаючи на кнопку, ми створюємо потік, він займає критичну секцію і починає роботу. Натискаємо другий - створюється другий потік, але критична секція зайнята, і він чекає, поки її не звільнить перший. Третій, четвертий - все пройдуть тільки по-черзі.

Критичні секції зручно використовувати при обробці одних і тих же даних (списків, масивів) різними потоками. Зрозумівши, як вони працюють, ви завжди знайдете їм застосування.

У цій невеликій статті розглянуті не всі способи синхронізації, є ще події (TEvent), а так само об'єкти системи, такі як м'ютекси (Mutex), семафори (Semaphore), але вони більше підходять для взаємодії між додатками. Усе інше, що стосується використання класу TThread, ви можете дізнатися самостійно, в Help'е все досить докладно описано. Мета цієї статті - показати початківцям, що не все так складно і страшно, головне розібратися, що є що. І побільше практики - найголовніше досвід!

Схожі статті

  • Потоки і робота з потоками

    Потоки і робота з потоками Далі .NET Framework поділяє процес операційної системи на полегшені керовані підпроцеси, що викликають домени додатки, представлені System. AppDomain.