Поиск…


Отзывчивый графический интерфейс с использованием потоков для фоновой работы и PostMessage для отчета из потоков

Сохранение графического интерфейса при длительном процессе требует либо очень сложных «обратных вызовов», чтобы позволить графическому интерфейсу обрабатывать свою очередь сообщений, либо использовать потоки (фоновые) (рабочие).

Уклонение от любого количества потоков для выполнения некоторых работ обычно не является проблемой. Веселье начинается, когда вы хотите, чтобы графический интерфейс показывал промежуточные и конечные результаты или сообщал о прогрессе.

Отображение чего-либо в графическом интерфейсе требует взаимодействия с элементами управления и / или очереди сообщений / насоса. Это всегда должно быть сделано в контексте основного потока. Никогда в контексте какой-либо другой темы.

Есть много способов справиться с этим.

В этом примере показано, как вы можете это сделать, используя простые потоки, позволяя графическому интерфейсу получить доступ к экземпляру потока после его завершения, установив FreeOnTerminate в значение false и сообщив, когда поток «сделан» с помощью PostMessage .

Примечания к условиям гонки: ссылки на рабочие потоки хранятся в массиве в форме. Когда поток завершен, соответствующая ссылка в массиве становится нулевой.

Это потенциальный источник условий гонки. Как и использование «Running» boolean, чтобы было легче определить, есть ли еще какие-то потоки, которые нужно закончить.

Вам нужно будет решить, нужно ли вам защищать этот ресурс с помощью блокировок или нет.

В этом примере, в его нынешнем виде, нет необходимости. Они изменяются только в двух местах: в StartThreads метода и HandleThreadResults метода. Оба метода выполняются только в контексте основного потока. Пока вы держите его таким образом и не начинаете называть эти методы из контекста разных потоков, у них нет возможности для создания условий гонки.

Нить

type
  TWorker = class(TThread)
  private
    FFactor: Double;
    FResult: Double;
    FReportTo: THandle;
  protected
    procedure Execute; override;
  public
    constructor Create(const aFactor: Double; const aReportTo: THandle);

    property Factor: Double read FFactor;
    property Result: Double read FResult;
  end;

Конструктор просто устанавливает частные члены и устанавливает FreeOnTerminate в False. Это необходимо, так как это позволит основному потоку запрашивать экземпляр потока для его результата.

Метод execute выполняет свой расчет, а затем отправляет сообщение в дескриптор, который он получил в своем конструкторе, чтобы сказать, что он сделан:

procedure TWorker.Execute;
const
  Max = 100000000;var
  i : Integer;
begin
  inherited;

  FResult := FFactor;
  for i := 1 to Max do
    FResult := Sqrt(FResult);

  PostMessage(FReportTo, UM_WORKERDONE, Self.Handle, 0);
end;

В этом примере важно использовать PostMessage . PostMessage «just» помещает сообщение в очередь сообщения сообщения основного потока и не ждет его обработки. Он асинхронный по своей природе. Если вы будете использовать SendMessage вы будете кодировать себя в рассол. SendMessage помещает сообщение в очередь и ждет, пока оно не будет обработано. Короче говоря, это синхронно.

Объявления для пользовательского сообщения UM_WORKERDONE объявляются как:

const
  UM_WORKERDONE = WM_APP + 1;
type
  TUMWorkerDone = packed record
    Msg: Cardinal;
    ThreadHandle: Integer;
    unused: Integer;
    Result: LRESULT;
  end;

UM_WORKERDONE const использует WM_APP в качестве отправной точки для своего значения, чтобы гарантировать, что он не мешает никаким значениям, используемым Windows или Delphi VCL (как рекомендовано MicroSoft).

форма

Любая форма может использоваться для запуска потоков. Все, что вам нужно сделать, это добавить к нему следующих членов:

private
  FRunning: Boolean;
  FThreads: array of record
    Instance: TThread;
    Handle: THandle;
  end;
  procedure StartThreads(const aNumber: Integer);
  procedure HandleThreadResult(var Message: TUMWorkerDone); message UM_WORKERDONE;

О, и примерный код предполагает существование Memo1: TMemo; в объявлениях формы, которые он использует для «ведения журнала и отчетности».

FRunning можно использовать для предотвращения щелчка GUI во время работы. FThreads используется для хранения указателя экземпляра и дескриптора созданных потоков.

Процедура запуска потоков имеет довольно простую реализацию. Он начинается с проверки того, есть ли уже набор ожидающих потоков. Если это так, он просто выходит. Если нет, он устанавливает флаг в значение true и запускает потоки, предоставляющие каждому свой собственный дескриптор, чтобы они знали, куда отправлять сообщение «сделано».

procedure TForm1.StartThreads(const aNumber: Integer);
var
  i: Integer;
begin
  if FRunning then
    Exit;
    
  FRunning := True;

  Memo1.Lines.Add(Format('Starting %d worker threads', [aNumber]));
  SetLength(FThreads, aNumber);
  for i := 0 to aNumber - 1 do
  begin
    FThreads[i].Instance := TWorker.Create(pi * (i+1), Self.Handle);
    FThreads[i].Handle := FThreads[i].Instance.Handle;
  end;
end;

Рукоятка потока также помещается в массив, потому что это то, что мы получаем в сообщениях, сообщающих нам, что поток выполнен и наличие его вне экземпляра потока облегчает доступ. Наличие дескриптора, доступного вне экземпляра потока, также позволяет нам использовать FreeOnTerminate для True если нам не нужен экземпляр для получения его результатов (например, если они были сохранены в базе данных). В этом случае, конечно, нет необходимости хранить ссылку на экземпляр.

Веселье в реализации HandleThreadResult:

procedure TForm1.HandleThreadResult(var Message: TUMWorkerDone);
var
  i: Integer;
  ThreadIdx: Integer;
  Thread: TWorker;
  Done: Boolean;
begin
  // Find thread in array
  ThreadIdx := -1;
  for i := Low(FThreads) to High(FThreads) do
    if FThreads[i].Handle = Cardinal(Message.ThreadHandle) then
    begin
      ThreadIdx := i;
      Break;
    end;

  // Report results and free the thread, nilling its pointer and handle 
  // so we can detect when all threads are done.
  if ThreadIdx > -1 then
  begin
    Thread := TWorker(FThreads[i].Instance);
    Memo1.Lines.Add(Format('Thread %d returned %f', [ThreadIdx, Thread.Result]));
    FreeAndNil(FThreads[i].Instance);
    FThreads[i].Handle := nil;
  end;

  // See whether all threads have finished.
  Done := True;
  for i := Low(FThreads) to High(FThreads) do
    if Assigned(FThreads[i].Instance) then
    begin
      Done := False;
      Break;
    end;
  if Done then
  begin
    Memo1.Lines.Add('Work done');
    FRunning := False;
  end;
end;

Этот метод сначала ищет поток, используя дескриптор, полученный в сообщении. Если совпадение найдено, оно извлекает и сообщает результат потока с помощью экземпляра ( FreeOnTerminate был False , помните?), А затем заканчивается: освобождение экземпляра и установка ссылки на экземпляр и дескриптор на нуль, указав, что этот поток не является более актуальным.

Наконец, он проверяет, работает ли какой-либо из потоков. Если ни один не найден, сообщается «все сделано», а флаг FRunning установлен на False чтобы можно было запустить новую партию работы.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow