Синхронизация потоков в 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;

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

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

  • метод (или простая функция), вызывающая 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;

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=21813