Багатопотоковість і Delphi

Нещодавно я досить негативно відгукувався про реалізацію класу TThread у Delphi із заявою, що потоки при завершенні іноді виснуть. Все ж таки тоді правда була не на моїй стороні. Просто потоки крутяться в різних DLL-ках, і цілком закономірно, що при розвантаженні за допомогою FreeLibrary бібліотеки, в якій крутяться потоки, відбуваються різні неприємні речі.

Основне правило, якого потрібно дотримуватися при реалізації багатопоточності в DLL: бібліотека повинна експортувати функцію очищення даних. Перед розвантаженням бібліотеки слід викликати цю функцію (вона зможе завершити всі потоки), а вже після виклику FreeLibrary().

Потоки неможливо знищити в секції завершення бібліотеки. Пробувати марно, можуть бути лише такі варіанти: - вінда зрубує потоки перед вивантаженням бібліотеки. І тут виникне зависання при виклику TThread.Free(), т.к. буде вічно очікувати завершення роботи неіснуючого потоку. Даний варіант відбувається при завершенні роботи процесу. - потоки не зрубуються. Вивантаження бібліотеки з працюючими потоками призводить до Access Violation, процес може завершитися. Якщо і EXE і DLL скомпільовані з пакетами, то вивантаження бібліотеки не призводить до AV, однак і потоки не завершуються.

Додатково: потоки не можна завершити в finalization DLL, оскільки, вивантаження бібліотеки і завершення потоку - взаємовиключні речі. Потік можна створити в initialization бібліотеки, однак Windows його не запустить, поки ініціалізація бібліотеки не завершиться (ці речі також взаємовиключні).

Все сказане жодним чином не відноситься до роботи EXE – там таких проблем не виникає.

Описана проблема розкривається в Хелпі (і спливала на тих чи інших форумах), але не всі з подібними явищамистикалися, тому для когось ця гілка може виявитися корисною.

> описані стандартні граблі новачків

Думаю, що й не лише новачків. Так само це стосується і професіоналів, які ще не встигли з яких-небудь причин прочитати MSDN на всі 100%.

Користуйтеся безпосередньо CreateThread - у чому проблема то :) Будете потім все валити на WinAPI замість TThread.

Можливо. Неможливо інше - дочекатися сигналу завершення потоку у функції очікування Пояснюється це досить просто - у момент фіналізації dll-модуля PEB поточного процесу заблоковано і система не може внести туди зміни, що стосуються TIB потоку, що завершився, а прапор завершення потоку система може підняти лише після внесення цих змін. Тобто. у цій ситуації виходить щось на зразок дідлока.

> Котик Б (24.04.08 9:26) [6] > Користуйтеся безпосередньо CreateThread - у чому проблема :)Скористайся - дізнаєшся.

ну що і потрібно довести. Пам'ятаю ту тему, де з апломбом стверджувалося, що потоки в Delphi реалізовані криво;) Смішно. Просто хтось не вміє їх готувати.

Перечитав Ріхтера (щодо багатопоточності). Але все ним написане я вже читав у Windows SDK. Тобто. Існує допустима деяка функція DllMain, до якої передається один з чотирьох параметрів. Наскільки я розумію, параметри DLL_THREAD_ATTACH і DLL_THREAD_DETACH у цій проблемі навряд чи допоможуть. А DLL_PROCESS_ATTACH і DLL_PROCESS_DETACH це теж, що і initialization і finalization в DLL (якщо я правильно зрозумів). Ну і що тоді "MS вже давно продумала"?

Щось незрозуміло, взагалі про що розмова. Потоки взагалі, не прив'язані до якоїсь конкретної біліотеки. До процесу прив'язані, якщо так можна сказати.

це ти зчого взяв? Потоки, створені в DLL - треба завершувати в DLL, а потоки, створені в іншому місці - ндадо завершувати у відповідному місці.

я не розумію, звідки ти отримуєш інформацію.

можливо, ти не розумієш як завершувати потоки в DLL?

Викликається FreeLibrary із програми. Відповідно, в контексті того ж потоку викликається DLL_PROCESS_DETACH (поговоримо поки що про нього, а не про DLL_THREAD_. ), і зважаючи на все викликається finalization модулів бібліотеки в Delphi.

Код обробки повинен бути наступний - запущеним потокам робиться Terminate, після чого йде WaitFor. функції, що очікують завершення цих потоків.

У самих потоках природно має бути своєчасна перевірка на Terminated властивість потоку, і якщо вона виставлена ​​- найкоротшим способом фіналізувати всі ресурси та видаляти потік.

Це – правильна ідеологія роботи.

> я не розумію, звідки ти черпаєш інформацію.

1. Зіткнувся з проблемою на практиці, зробив висновки 2. Підкріпив правильність висновків прочитанням Windows SDK та Ріхтера

> Код обробки має бути наступним - запущеним потокам > робиться Terminate, після чого йде WaitFor. функції, > які очікують завершення цих потоків. > > У самих потоках природно має бути своєчасна перевірка > на Terminated властивість потоку, і якщо вона виставлена ​​- найкоротшим > способом фіналізувати всі ресурси та видаляти потік. > > Це - правильна ідеологія роботи.

Але ж ця ідеологія не працює! Про це в [0] якраз і йдеться ;)

> це ти чого взяв? Потоки, створені в DLL – треба завершувати > в DLL, а потоки, створені в іншому місці - завершувати > у відповідному місці.

Якарізниця, де завершувати потоки (потоки Windows, а не Delphi TThread) ?

Примітно, що знищення потоку нормально відпрацьовує під час його створення функцією CreateThread. А AV при розвантаженні сиплються саме через BeginThread. Ця функція викликає CreateThread, але передає до неї замість вашої функції певну функцію ThreadWrapper. Очевидно, пов'язано саме з цим.

Проілюструй у коді ..

> Проілюструй у коді ..

uses Windows, Messages, SysUtils, Variants, Classes;

var Term: Boolean = False;

функція MyThreadFunc(Param: Cardinal): Integer; stdcall; begin Result := 0; while not Term do Sleep(100); Term := False; end;

procedure StartMythread; var I: Cardinal; begin I := 0; BeginThread(nil, 0, @MyThreadFunc, 0, 0, I); // Помилка є // CreateThread (nil, 0, @ MyThreadFunc, 0, 0, I); // Помилок немає end;

initialization StartMythread; finalization Term := True;

// Очікуємо завершення потоку while Term do Sleep(0); // AV валяться тут.

Sleep(0); // Чисто з метою налагодження end.

DLL і ЕХЕ скомпільовані без пакетів.

У ЇХІ дві кнопки: з LoadLibrary та FreeLibrary

"Ти ховраха бачиш? І я не бачу. А він є!" (с)

> // Очікуємо завершення потоку

Це зовсім не очікування завершення потоку, а лише очікування моменту коли змінна Term прийме значення False.

СаметутAV валитися не може, не вигадуй.

> BeginThread(nil, 0, @MyThreadFunc, 0, 0, I); // Помилка єПомилка у твоєму коді.

> Саме тут AV валитися не може, не вигадуй. Звідки тут AV взятися.

Адже я колисьтеж так думав! А виявляється, що є звідки. І саме на Sleep (0).

> BeginThread(nil, 0, @MyThreadFunc, 0, 0, I); // Помилка є

Дивись, що передаєш у BeginThread і що вона вимагає.

До речі, якщо писати таке без @, компілятор перевірятиме прототип.

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

Мда. Невдалий вийшов приклад. Соррі за неуважність: ( Але один фік з TThread AV гарантовано лізуть.

> один фік з TThread AV гарантовано лізуть

Ілюструй у коді тепер з TThread.

> Ілюструй у коді тепер із TThread..

uses Windows, Messages, SysUtils, Variants, Classes;

type TMyThread = class(TThread) protected procedure Execute; override; end;

var MyThread: TMyThread;

procedure StartMythread; begin MyThread := TMyThread.Create(False); end;

procedure TMyThread.Execute; begin inherited; //FreeOnTerminate := True; // Не впливає while not Terminated do Sleep(50); end;

initialization StartMythread; finalization // сценарій 1 //MyThread.Terminate; // з'являється AV

// сценарій 2 // MyThread.Free; // Програма зависає

// сценарій 3 //MyThread.Terminate; // Програма //MyThread.WaitFor; // зависає

// сценарій 4 // Нічого не робимо - виникає AV end.

Знову ж знайдете щось. :)

1,4 - нічого дивного, абсолютно очікуваний результат. 2,3 - див. [7] на предмет причини зависання

> 1,4 - нічого дивовижного, абсолютно очікуваного результату. > > 2,3 – див.[7] щодо причини зависання

Та щодо зависань якраз усе й зрозуміло. Windows SDK про це говорить цілком чітко.

Таким чином виходить, що в будь-якому випадку ми ловимо або AV, або зависання. А чи можна це якось обійти без додаткової експортованої функції? І як?

Обійти в принципі можна, але реалізація обходу буде вже з розряду трюків. А трюки, як відомо, штука ризикована і ненадійна)

до речі, у finalization такий трюк зазвичай рятує:

MyThread.Terminate; while Assigned(MyThread) do Sleep(20);

Але не завжди. У випадку, коли FreeLibrary бібліотеці не роблять, при завершенні роботи процесу все одно викликається для всіх DLL finalization, але до цього моменту вінда вже встигає зрубати всі потоки, тому цей код призводить лише до зависання.

> такий трюк зазвичай рятує

а навіщо в дельфі позбавляти себе такого зручного класу як TThread?

Наприкінці TMyThread.Execute (або деструкторі) виконується присвоєння MyThread := nil

Пробігав2. (25.04.08 13:40) [36]

Поява якоїсь там nil у якійсь там змінній не є фактом завершення потокової функції.

Це ті ж фаберже, що і в [19], тільки вид збоку)

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

о Боже. Ти взагалі розумієш, що робиш? Блін, і хтось говорив про професіоналізм. У методах класах звертатися до конкретного екземпляра класу?

я мабуть навіть відповідати не буду.