Multithreaded Application Tutorial/ru

From Free Pascal wiki
Jump to: navigation, 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. Многопоточное приложение - это приложение, которое создаёт два или более потока исполнения, работающих одновременно. Если Вы новичок в многопоточности, пожалуйста, прочитайте раздел "Нужна ли на самом деле многопоточность?" чтобы определить, что это действительно необходимо.

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

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

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

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

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

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

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

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

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

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

Класс TThread

Самый простой путь для создания многопоточного приложения - использование класса 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) останавливается. В противном случае, приложению будет необходимо освободить его вручную.

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

  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;

В приложении

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

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

  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;

В приложении

  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;

Важно знать

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

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

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


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

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

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

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

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

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

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 },

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

 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 файлах:

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

Модуль heaptrc

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

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

Поддержка SMP

Хорошие новости заключаются в том, что если Ваше многопоточное приложение работает правильно, SMP уже включено!

Отладка многопоточных приложений в Lazarus

Отладка в Lazarus всё ещё функционирует не полностью.

Отладка вывода

В однопоточном приложении Вы можете просто писать в консоль/терминал/что-то ещё и порядок строк будет сохранён таким, каким они были написаны. В многопоточном приложении всё сложнее. Если пишут два потока, и строка, написанная потоком A находится перед строкой, написанной потоком B, то строки не обязательно написаны в таком порядке. Может даже случиться так, что поток пишет на вывод, пока другой поток пишет строку (? It can even happen, that a thread writes its output, while the other thread is writing a line. ?)

Модуль LCLProc содержит несколько функций, позволяющих каждому потоку писать в свой собственный файл журнала(вести свои логи):

 procedure DbgOutThreadLog(const Msg: string); overload;
 procedure DebuglnThreadLog(const Msg: string); overload;
 procedure DebuglnThreadLog(Args: array of const); overload;
 procedure DebuglnThreadLog; overload;

Пример: Вместо writeln('Some text ',123); используйте

 DebuglnThreadLog(['Some text ',123]);

Это добавит линию 'Some text 123' в файл Log<PID>.txt, где <PID> - это идентификатор процесса данного потока.

Хорошей идеей является удаление файлов журнала перед каждым запуском:

 rm -f Log* && ./project1

Linux

Если Вы попытаетесь отладить многопоточное приложение на Linux, у Вас будет одна большая проблема: сервер X зависнет.\

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

Создать новый экземпляр Х:

 X :1 &

Он откроется, и когда вы переключитесь на другой рабочий стол (обычно нажатием Ctrl+Alt+F7), вы сможете вернуться к новому графическому рабочему столу с Ctrl+Alt+F8 (если эта комбинация не работает, попробуйте Ctrl+Alt+F2 ... Она работала на Slackware).

Тогда вы сможете, если вы хотите, создайте на рабочий стол сессии X:

 gnome-session --display=:1 &

Затем в Lazarus, в диалоге параметров запуска для проекта, выберите "Use display" и введите :1.

Теперь приложение будет работать на второй X сессии и вы сможете отлаживать её на первой.

Это было проверено с Free Pascal 2.0 и Lazarus 0.9.10 Windows и Linux.


Вместо создания нового сеанса X, Вы можете использовать Xnest. Xnest - это сеанс X в окне. С его помощью X сервер не блокируется во время отладки потоков, и поэтому гораздо проще отлаживать, не сохраняя изменения терминалов(?).

Командной строкой для запуска Xnest является:

 Xnest :1 -ac

создать X сессию на :1, и отключить контроль доступа.

Widgetsets

Интерфейсы win32, gtk и carbon полностью поддерживают многопоточность. Это значит, TThread, критические секции и Synchronize работают.

Критические секции

Критический раздел представляет собой объект, использующийся, чтобы удостовериться, что некоторые части кода выполняется только одним потоком одновременно. Критический раздел должен быть создан / инициализирован прежде чем он может быть использован и будет освобожден, когда он не нужен.

Критические секции обычно используются следующим образом:

Добавьте модуль SyncObjs.

Объявляем раздел (для всех потоков, которые будут использовать этот раздел):

 MyCriticalSection: TRTLCriticalSection;

Создаём секцию:

 InitializeCriticalSection(MyCriticalSection);

Запустим несколько потоков. Делаем что-нибудь исключительное(?):

 EnterCriticalSection(MyCriticalSection);
 try
   // доступ к переменным, запись файлов, отправка сетвых пакетов, и т.д.
 finally
   LeaveCriticalSection(MyCriticalSection);
 end;

После завершения всех потоков, освобождаем её:

 DeleteCriticalSection(MyCriticalSection);

В качестве альтернативы, вы можете использовать объект TCriticalSection. Создание делает инициализацию, метод Enter создаёт EnterCriticalSection, метод Leave создаёт LeaveCriticalSection и удаление делает деструкцию объекта.

Например: 5 увеличивающих счетчик потоков: См. lazarus/examples/multithreading/criticalsectionexample1.lpi

Осторожно: There are two sets of the above 4 functions. The RTL and the LCL ones. The LCL ones are defined in the unit LCLIntf and LCLType. Both work pretty much the same. You can use both at the same time in your application, but you should not use a RTL function with an LCL Critical Section and vice versus.

Общие переменные

Если неcколько потоков обращаются к переменной, которая предназначена только для чтения, то не о чем беспокоиться. Но если один или несколько потоков изменяют переменную, то необходимо убедиться в том, что в один промежуток времени только один поток имеет доступ к ней.

Пример: 5 потоков увеличивают переменную-счётчик. См. lazarus/examples/multithreading/criticalsectionexample1.lpi

Ожидание другого потока

Если потоку A необходим результат работы другого потока B, он должен ждать пока работа потока B будет завершена.

Важно: Главный поток никогда не должен ждать другой поток. Вместо этого используйте Synchronize (см. выше).

Пример: lazarus/examples/multithreading/waitforexample1.lpi

{ TThreadA }
 
procedure TThreadA.Execute;
begin
  Form1.ThreadB:=TThreadB.Create(false);
  // create event
  WaitForB:=RTLEventCreate;
  while not Application.Terminated do begin
    // wait infinitely (until B wakes A)
    RtlEventWaitFor(WaitForB);
    writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
  end;
end;
 
{ TThreadB }
 
procedure TThreadB.Execute;
var
  i: Integer;
begin
  Counter:=0;
  while not Application.Terminated do begin
    // B: Working ...
    Sleep(1500);
    inc(Counter);
    // wake A
    RtlEventSetEvent(Form1.ThreadA.WaitForB);
  end;
end;

Примечание: RtlEventSetEvent может быть вызвана перед RtlEventWaitFor. Тогда RtlEventWaitFor будет немедленно завершён (return). Используйте RTLeventResetEvent чтобы очистить флаг.

Fork (порождение)

При порождении в многопоточном приложении, следует знать, что любые потоки, созданные и запущенные ПЕРЕД вызовом fork (или fpFork), НЕ будут запущены в дочернем процессе. Как заявлено в справочной странице fork(), невозможно определить состояние любых потоков, которые были запущены перед вызовом fork().

Если есть какие-либо потоки, инициализированные перед вызовом (включая секцию инициализации), они не будут работать.

Параллельные процедуры/циклы

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

Распределённые вычисления

Следующим шагом после многопоточности является запуск потоков на различных машинах.

  • Вы можете использовать один из TCP компонентов, таких как synapse, lnet или indy для связи. Это даст Вам наибольшую гибкость и, в основном, используется для слабо связанных Клиент / Серверных приложений.
  • Вы можете использовать библиотеки передач сообщений, таких как MPICH, которые используются для высокопроизводительных вычислений (HPC, High Performance Computing) в кластерах.

Многопоточные компоненты