Multithreaded Application Tutorial/ru

From Lazarus wiki
Jump to navigationJump to search

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) polski (pl) português (pt) русский (ru) slovenčina (sk) 中文(中国大陆)‎ (zh_CN)

Обзор

Эта страница объясняет как писать и отлаживать многопоточные(multi-threaded) приложения средствами Free Pascal и Lazarus. Многопоточное приложение - это приложение, которое создаёт два или более потока исполнения, работающих одновременно. Если Вы новичок в многопоточности, пожалуйста, прочитайте раздел "Do you need multi-threading?" чтобы определить, что это действительно необходимо.

Один из потоков называется главным (Main Thread). Главный поток создаётся операционной системой после запуска приложения. Главный поток должен быть единственным потоком, который обновляет компоненты, взаимодействующие с пользователем: иначе, приложение может зависнуть.

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

Другое использование потоков состоит в лучшей отзывчивости (responding) приложения. Если вы создали приложение, где пользователь нажимает на кнопку, обрабатывающую большой объём работы... во время обработки экран перестаёт отвечать на запросы и пользователь думает, что приложение не отвечает. Создаётся плохое и вводящее в заблуждение впечатление. Если это задание выполняется во втором потоке, приложение сохраняет работоспособную форму (почти), какбудто оно находится в состоянии простоя. В этом смысле это хорошая идея перед запуском потока выключить кнопки на форме, позволяющие пользователю запустить более чем один поток для работы.

Другим способом использования многопоточности может быть серверное приложение, которое одновременно отвечает многим клиентам.

Нужна ли на самом деле многопоточность?

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

Многопоточные приложения всегда более сложные в отладке и зачастую они имеют гораздо более сложную структуру; во многих случаях Вам не нужна многопоточность. Одного потока вполне достаточно. Если Вы можете разделить трудоёмкую задачу на несколько небольших кусков, тогда вместо Вы должны использовать Application.ProcessMessages. Этот метод позволяет LCL обрабатывать все ожидающие сообщений и возвращения (messages and returns). Центральной идеей является вызов Application.ProcessMessages в одинаковые промежутки в ходе выполнения длительной задачи чтобы определить, нажал ли пользователь куда-нибудь или индикатор прогресса должен быть перерисован, и так далее.

К примеру, чтение большого файла и его обработки См. examples/multithreading/singlethreadingexample1.lpi.

Многопоточность необходима только для:

  • блокировки handles, таких как сетевое соединение
  • использования нескольких процессоров одновременно (multiple processors simultaneously, SMP)
  • вызовы алгоритмов и библиотек, которые должны вызываться через API, т.к. они не могут быть разделены на мелкие части.

Класс TThread

Следующий пример может быть найден в каталоге examples/multithreading/.

Самый простой путь для создания многопоточного приложения - использование класса TThread. Этот класс позволяет достаточно просто создать дополнительный поток (наряду с основным потоком). Обычно от вас требуется переопределить только 2 метода: конструктор Create, и метод Execute.

В конструкторе Вы подготавливаете поток для запуска. Вы устанавливаете начальные значения переменных и свойств, которые Вам нужны. Подлиннвый конструктор TThread требует параметр, называющийся Suspended (приостановить). Как и следует ожидать, установка Suspended = True будет препятствовать автоматическому запуску потоков после создания. Если Suspended = False, поток запускается сразу после создания. Если поток создан приостановленным (Suspended = True), тогда он запускается только после вызова метода Resume.

В FPC version 2.0.1 и более поздних, TThread.Create также имеет неявный параметр, отвечающий за размер стека. Теперь, если это необходимо, Вы можете изменить стандартный размер стека каждого потока, который Вы создали. Глубокая рекурсия в процедурах является хорошим примером. Если Вы не изменяете размер стека, используется стандартное размер стека операционной системы.

В переопределённом методе Execute Вы будете писать код, который запускается в потоке.

Класс TThread имеет одно важное свойство: Terminated : boolean;

Если поток имеет цикл (loop) (и это нормально), выход из цикла должен осуществиться, когда Terminated=true (по умолчанию - false). В каждом проходе значение свойство Terminated должно проверяться, и если оно равно true, цикл должен завершиться так быстро, как это возможно, после необходимой очистки. Имейте ввиду, что метод Terminate по-умолчанию ничего не делает: в метод Execute нужно явно организовать завершение задания.

Как мы объяснили ранее, поток не должен взаимодействовать с видимыми компонентами. Обновления для видимой компоненты должно быть сделаны в контексте главного, основного потока. Чтобы это сделать, у TThread существует метод, называющийся Synchronize. Synchronize требует метод в качестве аргумента. Когда вы вызываете метод через Synchronize(@MyMethod), запущенный поток приостанавливается, код метода MyMethod запускается в главном потоке, а затем выполнение потока будет продолжено. Точная обработка Synchronize зависит от платформы, но упрощённо он делает следующее: он посылает сообщение в главную очередь сообщений и приостанавливает своё выполнение(goes to sleep).В конечном итоге главный поток обрабатывает сообщение и вызывает MyMethod. Таким образом, MyMethod вызывается без контекста, то есть не во время события OnMouseDown или в течение события перерисовки (paint event), но после этого. После того, как главный поток запускает MyMethod, он будит спящий поток и обрабатывает следующее сообщение (событие?). Поток продолжает работать.

Существует другое важное свойство TThread: FreeOnTerminate. Если оно =true, тогда объект потока автоматически освобождается, когда выполнение потока (метод .Execute) останавливается. В противном случае, приложению будет необходимо освободить его вручную.

Пример:

<delphi>

 Type
   TMyThread = class(TThread)
   private
     fStatusText : string;
     procedure ShowStatus;
   protected
     procedure Execute; override;
   public
     Constructor Create(CreateSuspended : boolean);
   end;
 constructor TMyThread.Create(CreateSuspended : boolean);
 begin
   FreeOnTerminate := True;
   inherited Create(CreateSuspended);
 end;
 procedure TMyThread.ShowStatus;
 // этот метод запущен главным потоком и поэтому может получить доступ ко всем элементам графического интерфейса.
 begin
   Form1.Caption := fStatusText;
 end;

 procedure TMyThread.Execute;
 var
   newStatus : string;
 begin
   fStatusText := 'TMyThread Starting...';
   Synchronize(@Showstatus);
   fStatusText := 'TMyThread Running...';
   while (not Terminated) and ([любое необходимое условие]) do
     begin
       ...
       [здесь расположен код цикла главного (основного) потока]
       ...
       if NewStatus <> fStatusText then
         begin
           fStatusText := newStatus;
           Synchronize(@Showstatus);
         end;
     end;
 end;

</delphi>

В приложении

<delphi>

 var
   MyThread : TMyThread;
 begin
   MyThread := TMyThread.Create(True); // Таким способом он не запустится автоматически
   ...
   [Здесь расположен код, который инициализирует всё возможное, перед запуском потоков]
   ...
   MyThread.Resume;
 end;

</delphi>

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

<delphi>

 Type
   TShowStatusEvent = procedure(Status: String) of Object;
   TMyThread = class(TThread)
   private
     fStatusText : string;
     FOnShowStatus: TShowStatusEvent;
     procedure ShowStatus;
   protected
     procedure Execute; override;
   public
     Constructor Create(CreateSuspended : boolean);
     property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
   end;
 constructor TMyThread.Create(CreateSuspended : boolean);
 begin
   FreeOnTerminate := True;
   inherited Create(CreateSuspended);
 end;
 procedure TMyThread.ShowStatus;
 // этот метод запущен главным потоком и поэтому может получить доступ ко всем элементам графического интерфейса.
 begin
   if Assigned(FOnShowStatus) then
   begin
     FOnShowStatus(fStatusText);
   end;
 end;
 procedure TMyThread.Execute;
 var
   newStatus : string;
 begin
   fStatusText := 'TMyThread Starting...';
   Synchronize(@Showstatus);
   fStatusText := 'TMyThread Running...';
   while (not Terminated) and ([любое необходимое условие]) do
     begin
       ...
       [здесь расположен код цикла главного (основного) потока]
       ...
       if NewStatus <> fStatusText then
         begin
           fStatusText := newStatus;
           Synchronize(@Showstatus);
         end;
     end;
 end;

</delphi>

В приложении

<delphi>

 Type
   TForm1 = class(TForm)
     Button1: TButton;
     Label1: TLabel;
     procedure FormCreate(Sender: TObject);
     procedure FormDestroy(Sender: TObject);
   private
     { private declarations }
     MyThread: TMyThread; 
     procedure ShowStatus(Status: string);
   public
     { public declarations }
   end;
 procedure TForm1.FormCreate(Sender: TObject);
 begin
   inherited;
   MyThread := TMyThread.Create(true);
   MyThread.OnShowStatus := @ShowStatus;
 end;
 procedure TForm1.FormDestroy(Sender: TObject);
 begin
   MyThread.Terminate;
   MyThread.Free;
   inherited;
 end;
 procedure TForm1.Button1Click(Sender: TObject);
 begin
  MyThread.Resume;
 end;
 procedure TForm1.ShowStatus(Status: string);
 begin
   Label1.Caption := Status;
 end;

</delphi>

Важно знать

Проверка стека в Windows

Существует потенциальная проблема с потоками в Windows, если Вы используете переключатель (?) -Ct (stack check). По причинам не совсем понятным проверка стека "переключается" при любом TThread.Create если Вы используете размер стек по-умолчанию.

Единственный работающий способ на данный момент - просто не использовать переключатель -Ct. Заметим, что это не вызывает исключения в основном потоке, но вызывает в недавно созданном. Это "выглядит" как если бы поток никогда не был запущен.


Хороший код для проверки этого и других исключений, которые могут возникнуть в создании потоков: <delphi>

    MyThread:=TThread.Create(False);
    if Assigned(MyThread.FatalException) then
      raise MyThread.FatalException;

</delphi> Этот код будет гарантировать, что любое исключение, которое произошло во время создания потока будет распространёно на главный поток.

Модули, необходимые для мультипоточных приложений

Вам не нужно какого-либо специального модуля для работы в Windows. Однако, в Linux, Mac OS X и FreeBSD Вам нужен модуль cthreads и он должен быть первым использующимся модулем проекта (программного модуля, .lpr)!

Поэтому, код вашего приложения должен выглядеть так:

<Delphi> program MyMultiThreadedProgram; {$mode objfpc}{$H+} uses {$ifdef unix}

 cthreads,
 cmem, // the c memory manager is on some systems much faster for multi-threading

{$endif}

 Interfaces, // this includes the LCL widgetset
 Forms
 { you can add units here },

</Delphi>

Если Вы забыли про это, Вы получите следующую ошибку при запуске:

 This binary has no thread support compiled in.
 Recompile the application with a thread-driver in the program uses clause before other units using thread.

Пакеты, которые используют многопоточность, следует добавить флаг -dUseCThreads к обычным опциям использования. Откройте редактор пакетов, затем Options > Usage > Custom и добавьте -dUseCThreads. Таким образом Вы определите данный флаг для всех проектов и пакетов, использующих этот пакет. IDE и все новые приложения, созданные IDE уже будут иметь следующий код в их .lpr файлах: <Delphi> uses

 {$IFDEF UNIX}{$IFDEF UseCThreads}
 cthreads,
 cmem, // the c memory manager is on some systems much faster for multi-threading
 {$ENDIF}{$ENDIF}

</DELPHI>

Модуль heaptrc

Вы не можете использовать ключ -gh с модулем cmem. Ключ -gh переключает использование модуля heaptrc, который расширяет менеджер "кучи". Поэтому модуль heaptrc должен быть использован после модуля cmem.

<Delphi> uses

 {$IFDEF UNIX}{$IFDEF UseCThreads}
 cthreads,
 cmem, // the c memory manager is on some systems much faster for multi-threading
 {$ENDIF}{$ENDIF}
 heaptrc,

</Delphi>

SMP Support

Debugging Multi-threaded Applications with Lazarus

Debugging output

Linux

Widgetsets

Critical sections

Sharing Variables

Fork

Parallel procedures/loops