Difference between revisions of "Multithreaded Application Tutorial/ru"

From Lazarus wiki
(Stack checking under Windows)
Line 211: Line 211:
 
== Special things to take care of ==
 
== Special things to take care of ==
  
=== Stack checking under Windows ===
+
=== Проверка стека в Windows ===
 +
 
 +
Существует потенциальная проблема с потоками в Windows, если Вы используете переключатель (?) -Ct (stack check).
 +
По причинам не совсем понятным проверка стека "переключается" при любом TThread.Create если Вы используете размер стек по-умолчанию.
 +
 
 +
Единственный работающий способ на данный момент - просто не использовать переключатель -Ct. Заметим, что это не вызывает исключения в
 +
основной поток, но вызывает в недавно созданном. Это "выглядит" как если бы поток никогда не был запущен.
 +
 
 +
 
 +
Хороший код для проверки этого и других исключений, которые могут возникнуть в создании потоков:
 +
<delphi>
 +
    MyThread:=TThread.Create(False);
 +
    if Assigned(MyThread.FatalException) then
 +
      raise MyThread.FatalException;
 +
</delphi>
 +
Этот код будет гарантировать, что любое исключение, которое произошло во время создания потока будет распространёно на главный поток.
  
 
== Units needed for a multi-threaded application ==
 
== Units needed for a multi-threaded application ==

Revision as of 18:43, 6 December 2009

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>

Special things to take care of

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

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

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


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

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

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

Units needed for a multi-threaded application

Heaptrc

SMP Support

Debugging Multi-threaded Applications with Lazarus

Debugging output

Linux

Widgetsets

Critical sections

Sharing Variables

Fork

Parallel procedures/loops