Багатопоточність в своїх додатках. Частина 1

Джерело: webdelphi


Досить давно минув той час, коли використання багатопоточності в будь-яких серйозних програмах вважалося гарним тоном. На сьогоднішній день, це необхідність від якої дуже багато залежить, і в першу чергу – зручність використання програми. Майже будь-який сучасний програмний продукт (хоч винятки і можливі, я таких прикладів не знаю, той-же стандартний “калькулятор” при розрахунках використовує 2 потоку), будь він повністю реалізований в головному потоці, буде викликати у нас величезна кількість негативних емоцій всякий раз при роботі з мережею, файлами, та іншими ресурсоємними операціями.


Можна звичайно обійти питання використання потоків, застосовуючи в “затяжних” циклах метод Application.ProcessMessage, Дозволяючи додатком періодично обробляти чергу повідомлень. Але це значно сповільнить виконання циклу, а при роботі з мережею і зовсім не ефективно, оскільки більшість мережевих функцій порию дуже довго виконують свої запити.


Благо ще з WindowsNT і Delphi 6, у нас є можливість простої і зручної реалізації багатопоточності. В Delphi існує дві можливості роботи з потоками:


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


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


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


Робота з TThread


Перше і найважливіше що необхідно пам’ятати, то що кожен потік, в тому числі і головний, виконується в окремому адресному просторі. Це зовсім не означає що в потоці не можна використовувати константи, змінні і навіть функції оголошені в модулях програми. Без їх використання тіло потоку виросло б до непростимо великих розмірів. Однак необхідно пам’ятати що при ініціалізації потоку, в його область пам’яті, поміщаються всі використовувані ним змінні зі своїми початковими значеннями, і їх зміна ні яким чином не впливає на вміст їхніх аналогів в інших потоках. Більш того всі методи самого TThread, Які знаходяться в адресному просторі батьківського потоку, теж “поза його компетенцією”. Тут цілком логічно поява питання, навіщо взагалі потрібен об’єкт, який не може вплинути ні на роботу програми, ні навіть на самого себе в програмі?


Відповідь елементарна до смішного – “не потрібен”. Мабуть тому, розробники люб’язно надали нам можливість передачі змінних в потік через його метод 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.


Приклад реалізації потоку. Компонент TDownloader


В якості прикладу обрано компонент для завантаження файлів з Internet, у зв’язку з тривалістю виконання мережевих запитів.


Для скачування файлу в потік необхідно передати як мінімум два значення – урл файлу і Контейнер для отриманих даних. Посилання логічно передавати в змінній типу String, А контейнера краще використовувати TMemoryStream, Так-як він зберігає дані в незмінному вигляді, і дозволяє легко виводити їх у файл або TStrings. Але для спільної обробки контейнера передаємо тільки покажчик на нього.


{………………..}


type


  PMemoryStream = ^TMemoryStream;


{………………..}


private


fURL: String; / / урл


MemoryStream: TMemoryStream; / / наш “сіамських близнюків”


{………………..}


public


    constructor Create(CreateSuspennded: Boolean; const URL: String; Stream: PMemoryStream);


{………………..}


constructor TDownloadThread.Create(CreateSuspennded: Boolean; const URL: String; Stream: PMemoryStream);


begin


inherited Create (CreateSuspennded); / / метод предка


FreeOnTerminate: = True; / / очистка при знищенні


Pointer (MemoryStream): = Stream; / / прописуємо memorystream за отриманим адресою


fURL: = URL; / / запам’ятовуємо урл


end;


{………………..}


Далі визначаємося з самої скачкою, пишемо метод Execute.


{………………..}


procedure TDownloadThread.Execute;


var


pInet, pUrl: Pointer; / / дескриптори для роботи з WinInet


Buffer: array [0 .. 1024] of Byte; / / буфер для отримання даних


BytesRead: Cardinal; / / кількість прочитаних байт


  i: Integer;


begin / / тіло потоку


pInet: = InternetOpen (“Dowloader”, INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0) ;/ / відкриваємо сесію


  try


pUrl: = InternetOpenUrl (pInet, PChar (URL), nil, 0, INTERNET_FLAG_PRAGMA_NOCACHE or INTERNET_FLAG_RELOAD, 0) ;/ / стукаємося по посиланню.


repeat / / качаємо файл


if Terminated then / / якщо потік необхідно завершити


Break; / / у даному випадку просто виходимо з циклу


FillChar (Buffer, SizeOf (Buffer), 0); / / заповнюємо буфер нулями


if InternetReadFile (pUrl, @ Buffer, Length (Buffer), BytesRead) then / / читаємо шматок в буфер


MemoryStream.Write (Buffer, BytesRead) / / пишемо буфер в потік


until (BytesRead = 0); / / прочитано всі


MemoryStream.Position: = 0; / / позицію потоку в нуль


  finally


if pUrl nil then / / відкривалося?


InternetCloseHandle (pUrl); / / закриваємо


if pInet nil then / / відкривалося?


InternetCloseHandle (pInet); / / закриваємо


  end;


pointer (MemoryStream): = nil; / / обриваємо зв’язок


end;


{………………..}


Начебто-б все? Все так не всі), а як-же Synchronize? А їх я написав трохи багато), і вирішив сюди не викладати. Так-що для завершення вивчення даного питання, качайте файлик. Крім самого компонента в архіві приклад його використання, і приклад роботи з pointer описаний вище.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*