Синхронизация потоков в DelphiИсточник: delphiplus Вячеслав Минияров
Атор: Вячеслав Минияров, Delphi Plus Одно время пришлось плотно работать с потоками, причём с десяток методов требовали переключения в основной поток. Самое неприятное, что методы все с параметрами, причем разными. Наверняка я был не первый, кто объявлял массив вариантов, в который записывал параметры вызова, затем Synchronize(Caller), а в Caller - вызов той же функции с параметрами из массива. В конце концов мне это надоело: ведь все параметры вызываемого метода уже лежат в стеке! Стал думать, и вот что получилось: Сначала - как это выглядит в коде: // создаём и запускаем поток procedure TForm1.Button1Click(Sender: TObject); begin Th:= TVThread.Create; end; procedure TVThread.Execute; var s: string; begin // Form1.Test должна выполняться в контексте главного потока s:= Form1.Test(123, 'Abcd'); Form1.Hint:= s; end; // параметры - любого типа, результат - тоже function TForm1.Test(p1: integer; p2: string): string; begin if Th.SFork then exit; // собственно, СИНХРОНИЗАЦИЯ ...... // код для выполнения в основном потоке ...... end; Симпатично? Мне нравится :) А теперь о грустном:
Всё от того, что адрес точки входа в вызывающий метод можно определить только при прямой адресации и никак - при косвенной. :( Естественно, Th.Fork(Pointer) можно использовать и для статических методов. Реализация: Метод Fork (SFork) сначала проверяет, в каком потоке его вызвали. Если это - главный поток, то ничего не делает, возвращает FALSE. Иначе вычисляет по содержимому стека начало и размер блока параметров вызвавшей его процедуры и запоминает эти данные в своих полях. Затем через стандартный Synchronize вызывается метод _Fork. _Fork, выполняясь уже в контексте главного потока, резервирует в стеке место, копирует в него блок параметров из стека заторможенного в это время потока и вызывает снова тот же метод (TForm1.Test в нашем примере). Этот метод теперь будет выполнен до конца, т.к. теперь Fork вернёт false. По поводу безопасности кода: т.к. оба раза выполняется код входа в метод и код выхода из него, то все строки и пр. подобные типы, переданные как параметры, нормально увеличивают/уменьшают счётчики ссылок и нормально же отрабатывают автоматически добавляемые компилятором блоки try..finally..except. TVThread = class(TThread) private FRC: dword; // адрес синхронизуемого метода FStart: dword; // начало блока параметров этого метода FCount: dword; protected procedure _Fork; procedure ErrorCall; dynamic; procedure DoTerminate; override; procedure Execute; override; // длина этого блока public function Fork(Func: Pointer): boolean; function SFork: boolean; constructor Create; // только для статических методов! end; procedure TVThread._Fork; var savsi: dword; savdi: dword; asm mov savsi, ESI // сохранить регистры mov savdi, EDI mov ESI, [EAX].FStart // src ptr mov ECX, [EAX].FCount mov EDX, [EAX].FRC sub ESP, ECX mov EDI, ESP // dst ptr rep movsb // копировать блок параметров mov ESI, savsi // восстановить регистры mov EDI, savdi call EDX // вызвать синхронизуемый метод end; function TVThread.Fork(Func: Pointer): boolean; begin asm mov result, false push EAX // save @Self call GetCurrentThreadId pop ECX // ECX = @Self cmp EAX, MainThreadID jz @@nothing // если осн. поток - выход mov result, true mov EAX, Func mov [ECX].FRC, EAX // запомнить адрес метода mov EAX, [EBP] // EAX = EBP метода Func // EBP перед вызовом Func и адрес возврата из Func нам не нужны lea EDX, [EAX+8] mov [ECX].FStart, EDX // начало блока параметров mov EAX, [EAX] // ESP перед вызовом Func = конец блока sub EAX, EDX // длина блока mov [ECX].FCount, EAX // запомнить длину mov AL, true // продублировать result в AL @@nothing: end; // Synchronize лучше вызывать вне ASM блока, т.к. // компилятор тогда сам (и правильно) создаст // try..finally обрамление в операторах begin и end if result then Synchronize(_Fork); end; function TVThread.SFork: boolean; var error: boolean; begin asm mov result, false push EAX // save @Self call GetCurrentThreadId pop ECX // ECX = @Self cmp EAX, MainThreadID jz @@nothing // если осн. поток - выход mov result, true mov error, true mov EAX, [EBP] // EAX = EBP вызвавшей процедуры mov EAX, [EAX+4] // EAX = адрес возврата из неё // вызов статического метода на ассемблере выглядит так: // первый байт - E8 (call), далее четыре байта - смещение // относительно адреса следующей инструкции. // адрес возврата указывает именно на эту следующую инструкцию cmp byte ptr [EAX-5],0E8h // проверить способ вызова jne @@nothing // Ошибка! Не прямой вызов. mov error, false add EAX, [EAX-4] // EAX = caller entry point mov [ECX].FRC, EAX // сохранить адрес вызвавшей процедуры mov EAX, [EBP] // далее всё идентично Fork lea EDX, [EAX+8] mov [ECX].FStart, EDX mov EAX, [EAX] sub EAX, EDX mov [ECX].FCount, EAX mov AL, true @@nothing: end; if not result then exit; if error then begin ErrorCall; // в наследнике можно обругаться по своему // если Exception всё же не был вызван в ErrorCall.. raise Exception.Create( 'TVThread.SFork: Error Calling conventions!'); end else Synchronize(_Fork); end; procedure TVThread.ErrorCall; begin end; |