Синхронизация потоков

© 2004 Вячеслав Минияров

Одно время пришлось плотно работать с потоками, причём с десяток методов требовали переключения в основной поток. Самое неприятное, что методы все с параметрами, причем разными. Наверняка я был не первый, кто объявлял массив вариантов, в который записывал параметры вызова, затем 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;

Симпатично? Мне нравится :)

А теперь о грустном:

  • метод (или простая функция), вызывающая SFork, не может быть типа register: только pascal, cdecl или stdcall (почему - будет ясно ниже);
  • если метод не статический (виртуальный или динамический) или вызван не напрямую (через переменную процедурного типа, например, или это Event), то вместо простого и красивого SFork придётся вызывать Fork:
    
    function TForm1.Test(p1: integer; p2: string): string;
    begin
     if Th.Fork(@TForm1.Test) then exit;
    ....
    
    с передачей ему адреса синхронизуемого метода.

Всё от того, что адрес точки входа в вызывающий метод можно определить только при прямой адресации и никак - при косвенной. :(

Естественно, 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;

Исходные тексты (2.37K)

Copyright© 2004 Минияров Вячеслав Юрьевич Специально для Delphi Plus



Новости за месяц

  • Декабрь
    2019
  • Пн
  • Вт
  • Ср
  • Чт
  • Пт
  • Сб
  • Вс
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31