Нить в своїх додатках. Частина 1, Різне, Програмування, статті

Джерело: webdelphi


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


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


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


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


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


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


Робота з 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>

*

*