Багатопотокові додатки. Створення потоків. Синхронізація подій

У Delphi існує дві можливості роботи з потоками:

1.Взаімодействіе через ідентифікатор, отриманий при створенні потоку функцією createthread.

2.Созданіе нащадка класу TThread і використання його методів.

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

Робота з TThread

Кожен потік, в тому числі і головний, виконується в окремому адресному просторі. Це зовсім не означає що в потоці не можна використовувати константи, змінні і навіть функції оголошені в модулях програми. Без їх використання тіло потоку виросло б до непростимо великих розмірів. Однак необхідно пам'ятати що при ініціалізації потоку, в його область пам'яті, поміщаються всі використовувані ним змінні зі своїми початковими значеннями, і їх зміна ні яким чином не впливає на вміст їхніх аналогів в інших потоках. Більш того всі методи самого TThread, які знаходяться в адресному просторі батьківського потоку, теж "поза його компетенцією".

Розробники Delphi надали нам можливість передачі змінних в потік через його метод Create, просто оголосивши їх у методі нащадка.

{...................}

constructor Create (CreateSuspennded: Boolean; MyVar: Integer); override;

{...................}

(CreateSuspennded змінна оголошена в методі TThread, про її призначення трохи нижче.)

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

{...................}

var

list1, list2: tstrings;

begin

list1: = TStringList.Create; / / створюємо перший аркуш

pointer (list2): = pointer (list1); / / міняємо адресу у Лист2 на Лист1

list2.Add ('Hello World!'); / / додаємо рядок на зразок-б як у другій лист

ShowMessage (list1.strings [0]); / / виводимо вміст із першого аркуша, воно-таки вміст другого)

list2.destroy; / / знищуємо другий лист, він же перший, повторний виклик попередньої рядки викличе помилку

end;

{...................}

Основний принцип роботи з даними вродеби зрозумілий. Вводимо дані за допомогою pointer, обробляємо і виводи дім. При створенні потоку, в його тіло поміщається його ж метод Execute. Ось в ньому те і потрібно проводити всі необхідні обчислення. Виклик методу Execute безпосередньо не запустить новий потік а просто виконає його в поточному, що в більшості випадків хоч і не викличе помилки, але як мінімум "підвісить" програму на час його виконання. Щоб запустити цей метод в окремому потоці, необхідно викликати інший його метод, метод Resume який проробляє всю необхідну роботу по ініціалізації і запуску потоку. У випадку якщо необхідно тимчасово призупинити роботу потоку використовується метод Suspend. При цьому потік буде знаходиться в стані "Паузи", до повторного виклику Resume.

Повернемося до конструктору, який вже згадувався вище. Спочатку, для TThread, він виглядає так:

constructor Create (CreateSuspennded: Boolean);

Параметр CreateSuspennded призначений, як випливає з назви, для створення потоку в припиненому вигляді. Для завдання деяких методів створюваного екземпляра класу. Наприклад, такого какFreeOnTerminate: Boolean який, також дотримуючись з назви, вказує класу на необхідність вивільнення пам'яті, займаної потоком після його знищення.

Обрабитавать дані в потоці, ми вже вміємо. Можна йти далі? Можна, але є ще один момент, забувати про який не можна ні в якому разі, це "спільний доступ до пам'яті". Імовірність того що кілька різних потоків (включаючи головний) одночасно будуть працювати з однією змінною в окремих випадках досить мала.

Для того что-б повідомити, що викликав потоку, про певний етап роботи потоку використовується методSynchronize (AMethod: TThreadMethod), що викликається усередині потоку. При цьому параметром AMethod виступає метод нашого нащадка TThread не має параметрів. Цей метод буде виконаний в "батьківському" потоці, при цьому значення властивостей об'єкта TThread будуть синхронізовані для цих потоків, а робота нашого потоку припинена до завершення методу Synchronize. Таким чином можна повідомляти програмі про виникаючі помилки, передавати проміжні значення обчислень, повідомляти про завершення роботи потоку, а також вносити корективи в його роботу. Після завершення методу синхронізації потік повертається до своєї роботи, якщо звичайно в Synchronize він не був припинений або знищений.

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

function TerminateThread (hThread: THandle; dwExitCode: DWORD): BOOL;

Хендл потоку можна отримати методом ThreadID, а в dwExitCode достатньо вказати '0 ', в разі успішного знищення функція поверне True.

Коректно завершити роботу потоку можна лише дочекавшись закінчення роботи методу Execute. Але що якщо нам не дуже хочеться чекати поки потік повністю відпрацює свою "програму". Для цього передбачений метод Terminate .. Все що він робить це задає властивості Terminated, нашого компонента в потоці, значення True .. Достатньо в кожному затяжному циклі, або після особливо тривалих функцій і процедур, вставити перевірку значення цієї змінної, і вслучае якщо вона дорівнює True завершувати роботу коректно, вивільнити пам'ять від створених об'єктів, закрити відкриті мережеві підключення, або просто відразу викликати Exit.