1
0
mirror of https://github.com/loginov-dmitry/multithread.git synced 2025-02-16 09:21:46 +02:00
multithread/multithread_in_delphi_for_beginners.md

2137 lines
204 KiB
Markdown
Raw Normal View History

# Многопоточное программирование в Delphi для начинающих
2020-08-15 14:49:20 +03:00
(редакция 1.3 от 15.08.2020г)
2020-07-31 22:49:03 +03:00
Автор: Логинов Д.С.
Пенза, 2020
<!-- Редактирование md-файла выполнялось в notepad++ версии 7.8.9.
Подсведка синтаксиса markdown в нём включена по умолчанию.
markdown-редакторы не подошли, т.к. они либо не удобные, либо не
справляются с таким объёмом материала
2020-07-31 22:49:03 +03:00
Возможные темы для дальнейшей работы над статьёй:
- упомянуть способ объявления переменных потока threadvar
- обработка исключений в потоках
- логгирование событий, происходящих в потоках
- передача данных в доп. поток: использование очереди Windows
- передача данных в доп. поток: использование очереди RTL
- передача данных в доп. поток: пример организации записи в лог на удалённый сервер:
множество потоков генерируют события, один поток передаёт на сервер через одно подключение
- можно попробовать наглядно показать преимущество использования Эвента по сравнению со Sleep(20).
- работа с базами данных: каждый поток должен обращаться к БД через своё подключение. Однако могут быть компоненты,
которые поддерживают множество подключений к БД через одно TCP-соединение, учесть это как-то!
- синхронизация потоков (крит. секции подробнее, мьютексы, эвенты, семафоры,
TMonitor в режиме крит. секции и в режиме эвента; также может быть упомянуть MREWS)
- пример навешивания модального окна ожидания окончании операции в доп. потоке
- Упомянуть об использовании Screen.Cursor, А также об HungAppTimeout как о средствах, позволяющих
в некоторых случаях обойтись без доп. потока.
- пример создания анонимного потока в современных Delphi
- пример программирования в PPL. Нужно дать ссылки на статьи и видеоуроки в интернете
- подробнее об OmniThreadLibrary - сначала нужно дождаться кроссплатформенной версии
2020-07-28 - начало работы над markdown-версией
-->
# Оглавление
1. Вступление [...](#intro)
1.1 Для чего используется многопоточное программирование в Delphi [...](#multithread_delphi_reason)
1.2 В чём сложность разработки многопоточных программ в Delphi по сравнению с другими современными языками программирования[...](#multithread_delphi_difficult)
1.3 Поддержка многопоточности в операционных системах и процессорах [...](#multithread_os)
2. Базовый класс многопоточности - TThread [...](#class_tthread)
3. Предотвращаем зависание основного потока (имитация задачи длительного вычисления) [...](#app_hungup)
4. Управление временем жизни потоков [...](#thread_lifetime_control)
4.1 Использование свойства TThread.Terminated для выхода из метода Execute [...](#use_terminated)
4.2 Главная форма отвечает за прекращение работы потока и уничтожение объекта потока [...](#mainform_is_owner)
4.3 Главная форма отвечает за уничтожение объекта потока с разовой задачей [...](#mainform_is_owner_short_thread)
4.4 Главная форма отвечает за уничтожение нескольких долгоживущих потоков [...](#mainform_is_owner_some_thread)
4.5 Использование списка TObjectList для хранения ссылок на объекты потоков [...](#use_object_list)
4.6 Простой пример организации паузы в работе потока [...](#thread_pause)
4.7 Использование свойства FreeOnTerminate для автоматического уничтожения объекта потока при завершении работы потока [...](#free_on_terminate)
4.8 Организация корректного завершения работы программы при использовании свойства FreeOnTerminate[...](#free_on_terminate_correct_exit)
5. Передача информации из дополнительного потока в основной [...](#send_info_to_main_thread)
5.1 Обращение к визуальным компонентам формы из дополнительного потока – как нельзя делать [...](#vcl_not_threadsafe)
5.2 Использование метода TThread.Synchronize для передачи данных в основной поток [...](#use_synchronize)
5.3 Периодическое чтение основным потоком данных, подготовленных в дополнительном потоке [...](#read_data_by_timer)
5.4 Использование функции SendMessage для передачи данных в основной поток [...](#use_sendmessage)
5.5 Использование функции PostMessage для передачи очереди сообщений в основной поток [...](#use_postmessage)
5.6 Использование списка TThreadList для передачи очереди данных в основной поток [...](#use_thread_list)
5.7 Использование метода TThread.Synchronize для передачи данных в основной поток – вариант с использованием анонимной процедуры [...](#use_synchronize_anonimous)
5.8 Использование метода TThread.Queue для передачи данных в основной поток [...](#use_tthread_queue)
6. Планирование потоков [...](#os_scheduler)
6.1 Роль системного таймера [...](#os_timer)
<!--6.2 Роль кэшей процессора [...](#cpu_cache)-->
<!--6.2 Стоимость переключения контекста потоков [...](#context_cost)-->
6.2 Влияние количества запущенных потоков на производительность системы [...](#thread_count_cost)
6.3 Приоритеты потоков в Windows [...](#about_priority_in_windows)
7. Где можно и где нельзя создавать и уничтожать потоки [...](#bad_places_for_create_free_threads)
2020-07-31 22:49:03 +03:00
8. Немного о threadvar [...](#about_threadvar)
# 1. Вступление <a name="intro"></a>
В данной работе (условно назовём её «Учебник») речь пойдет о многопоточном программировании в Delphi. Учебник написан для сообщества Delphi-программистов (к этому сообществу следует отнести также Lazarus-программистов). В дальнейшем я буду использовать термин «программист», подразумевая именно Delphi-программиста.
В современных версиях Delphi (например, Delphi 10.3) имеется удобная библиотека многопоточного программирования «Parallel Programming Library» и в последние годы всё больше примеров в интернете даётся именно для PPL. Она хорошо подходит для тех задач, где требуется распараллелить математические вычисления либо выполнить параллельную однотипную обработку информации во множестве файлов на SSD-накопителе. На самом деле, возможности PPL ограничены только фантазией программиста.
Я же постараюсь сделать основной упор на классическом способе многопоточного программирования в Delphi – работе с классом TThread (вернее, с классами-наследниками, которые разрабатывают программисты для решения тех или иных задач). Считаю, что именно классический подход должен оставаться основным (по крайней мере, программисты должны уметь применять этот подход на практике и должны ориентироваться в чужом коде, где применяется классический подход).
К сожалению, в интернете (в открытых источниках) очень мало качественного материала, который поможет начинающему программисту научиться работать с классом TThread. Формально, материала полно, однако большая его часть, вопреки стараниям разработчиков этого материала, не учит многопоточному программированию, а чаще сбивает с толку, в том числе из-за множества ошибочных примеров и советов, которые нельзя использовать для данной темы.
Профессиональные программисты вероятно не найдут в данной работе ничего нового. Каких-либо научных открытий здесь не планируется. В отличие от научных работ, я не планирую увлекаться ссылками на различные известные источники. Вы всегда можете уточнить любой вопрос с помощью поисковой машины.
Местами Вы будете встречать несколько неуклюжую терминологию, объяснения «на пальцах». Сделано так специально, чтобы не раздувать объём материала и не мучать читателя длинными определениями общеизвестных терминов.
Я постараюсь использовать максимально простой стиль изложения материала, надеюсь, начинающие программисты оценят.
## 1.1 Для чего используется многопоточное программирование в Delphi <a name="multithread_delphi_reason"></a>
Я хотел бы выделить две основные причины использования многопоточности в Delphi-программах:
1. Длительные математические вычисления и обработка данных. Примеры: шифрование данных, сжатие данных, анализ данных, конвертация видео или звука, обучение нейронных сетей и многое другое. Для всех этих задач характерна высокая и длительная нагрузка на процессор. Очевидно, что если мы при разработке обычного VCL-приложения попытаемся решить такую задачу без вынесения в отдельный поток, то интерфейс пользователя подвиснет на время, которое требуется для выполнения вычислений.
2. Задачи длительного ожидания завершения ввода-вывода. Примеры: выполнение HTTP-запроса (или REST-запроса) к какому-либо сайту в интернете, обмен данными по протоколу TCP, обмен с устройством по COM-порту, запуск и ожидание работы других программ на компьютере и многое другое.
Для всех этих задач характерна низкая нагрузка на процессор. Ресурсы процессора практически не задействуются, однако наша программа после запуска операции будет вынуждена ожидать её завершения. Очевидно, что если мы при разработке обычного VCL-приложения попытаемся решить такую задачу без вынесения в отдельный поток, то интерфейс пользователя подвиснет на время, которое требуется для ожидания завершения операции ввода-вывода.
## 1.2 В чём сложность разработки многопоточных программ в Delphi по сравнению с другими современными языками программирования <a name="multithread_delphi_difficult"></a>
В современном программировании принято разделять разрабатываемую систему на бэкенд и фронтэнд. Бэкенд – это сервис, который отвечает за хранение данных, их обработку и взаимодействие с фронтэндом. Фронтэнд – это программа, представляющая интерфейс пользователя. Она, как правило, не хранит и не обрабатывает данные. Её цель – обеспечить для пользователя наиболее удобную работу.
Delphi – это прежде всего инструмент для разработки фронтэнда, поскольку мы создаём в первую очередь программы с графическим интерфейсом пользователя (GUI). Программист фронтэнда должен сделать всё возможное, чтобы пользователю было максимально комфортно работать. Никаких подвисаний интерфейса во фронтэнде быть не должно! Однако этого достичь в Delphi весьма нелегко. Если мы запрашиваем с сервера данные без использования многопоточности, то интерфейс программы подвисает на время обработки запроса. Некоторые сетевые библиотеки позволяют частично обойти данную проблему (например, режим антизаморозки в Overbyte ICS или Indy), однако далеко не всегда данный режим применим.
Основной режим работы в библиотеке Overbyte ICS – асинхронный, при котором весь сетевой обмен выполняется в рамках одного потока, этот поток не блокируется (т.е. не подвисает), однако программист должен самостоятельно реализовать обработчики различных событий (например, обработчик события получения данных). Если протокол обмена с сервером состоит из множества запросов и ответов, то наша программа значительно усложняется, мы можем легко столкнуться с явлением «callback hell» (ад функций обратных вызовов). Конечно, профессиональный программист такого не допустит и сможет реализовать машину состояний (конечный автомат), которая избавит от проблемы «callback hell», но профессионалы, к сожалению, на дороге просто так не валяются. Напомню, статья предназначена в первую очередь для новичков, которые вряд ли смогут легко справиться с этой задачей. К тому же, даже если мы и реализуем асинхронный сетевой обмен данными в рамках одного потока, мы столкнемся с необходимостью обработки принятых данных, необходимостью выполнять запросы к базе данных, работать с файлами (на медленном HDD), а в конечном итоге – к подвисаниям интерфейса пользователя. Знание многопоточного программирования нам помогает справится с этой проблемой.
У некоторых других современных языков программирования (C#, JavaScript, Rust, Kotlin и т.д., их число постоянно растёт) проблема «callback hell» решена на уровне языка: введён встроенный механизм асинхронности: async / await. Смысл в том, что программист на этих языках пишет простой последовательный код (без обработчиков событий), а компилятор автоматически преобразует этот код в асинхронный с использованием машины состояний. Т.е. программист на этих языках теперь не сталкивается с теми сложностями, с которыми сталкиваемся мы при использовании асинхронных сетевых библиотек (кстати, это не только Overbyte ICS, но и ещё десяток других, в том числе коммерческих).
К слову, программист на языке JavaScript, как правило, не сталкивается с многопоточностью. Практически все вещи в JavaScript можно реализовать в контексте одного (главного) потока с использованием возможностей асинхронности. Конечно, асинхронность не спасёт в ситуациях, когда требуется выполнить длительные математические расчёты или обработку данных (интерфейс пользователя будет подвисать на время такой обработки), однако такую работу выполняет, как правило, бэкэнд.
Мы же используем Delphi (на мой взгляд – наиболее мощный инструмент для разработки кроссплатформенных GUI-приложений практически для любой платформы) и вынуждены пользоваться теми возможностями, которые этот инструмент нам предлагает, а встроенной асинхронности у данного инструмента пока нет.
Давайте же перейдём к теме многопоточности!
## 1.3 Поддержка многопоточности в операционных системах и процессорах <a name="multithread_os"></a>
Буду говорить своими словами, очень упрощённо. Обязательно почитайте материал на эту тему в Интернете (очень много классных статей на эту тему есть на ресурсе habr.com, например, в статьях, посвящённых языку C#).
Я буду говорить в первую очередь о компьютерах с ОС Windows и процессорах с архитектурой Intel x86 (либо AMD x86_64), однако аналогичные механизмы есть в различных устройствах, оснащённых ОС Linux, Android, MacOS, iOS и процессорами, на которых работают эти ОС.
Итак, на компьютере одновременно запущены сотни программ (приложений). Мы называем их также «процессы». Процесс (process) – это объект операционной системы. Когда мы запускаем программу, ОС создаёт объект «процесс». При создании процесса ОС создаёт также объект «поток» (thread), для него создаётся стек вызовов (call stack) и код запускается на исполнение. Исполняемый код всегда выполняется в контексте какого-то одного потока. В любом процессе всегда есть минимум один поток. При необходимости, в процессе может быть более одного потока (например, в процессе WEB-сервера могут быть созданы тысячи потоков).
В типовом приложении исполняемый код 99% времени занимается тем, что ожидает поступления каких-либо событий. Как только событие поступило, исполняемый код «просыпается» и выполняет действия по обработке этого события, после чего «засыпает» до тех пор, пока не произойдет следующее событие. Таким образом, на компьютере могут быть созданы одновременно десятки тысяч потоков, которые 99% времени находятся в спящем состоянии и не оказывают значительной нагрузки на процессор.
Важно знать, что в ОС присутствует системный планировщик задач (system scheduler), который отвечает за распределение процессорного времени между потоками. В том случае, если поток выполняет длительную вычислительную задачу (обрабатывает информацию), планировщик задач автоматически выполнит действия, необходимые для перевода данного потока в спящий режим, как только поток истратит выделенный ему квант времени (предположим, 32 мс). Кстати, в большинстве случаев потоки намного раньше переходят в спящий режим, т.к. выполняют свою задачу раньше, чем успевает закончиться выделенный квант времени (time slice), либо переходят в спящий режим в связи с ожиданием завершения ввода-вывода.
Как только поток переходит в спящий режим, производится поиск следующего потока, готового в выполнению, далее осуществляется восстанавление его контекста и возобновление работы.
Каждый раз, когда поток переходит в спящий режим, происходит сохранение его контекста (сохраняется текущее состояние регистров процессорного ядра, на котором поток выполняется). При переводе потока в рабочий режим выполняется обратное действие – восстановление контекста потока (загрузка ранее сохраненных значений в регистры процессорного ядра). *Я даю здесь объяснение «на пальцах», а Вы, если интересно, поищите в интернете более подробную информацию.*
Кстати, операция переключения контекста по времени далеко не бесплатная, поэтому производители процессоров и разработчики ОС постоянно делают улучшения для повышения скорости данной операции. Если Вы хотите создать эффективный высокопроизводительный WEB-сервер, то будет не лишним подумать над тем, как минимизировать количество переключений контекста при исполнении Вашего кода. *Я не буду давать рекомендаций по поводу создания высокопроизводительного WEB-сервера, поскольку задача эта больше подходит для специализированных языков программирования WEB-серверов, например GoLang, а Delphi заточен в первую очередь на разработку фронтэнда, а также корпоративного программного обеспечения.*
# 2. Базовый класс многопоточности - TThread <a name="class_tthread"></a>
`TThread` – это базовый класс, инкапсулирующий функционал для обеспечения работы параллельного потока. Если мы хотим реализовать код, который должен выполняться в отдельном потоке, то нам необходимо реализовать наследника от класса `TThread` и, как минимум, реализовать override-метод Execute, который перекроет виртуальный абстрактный метод `Execute`, объявленный в родительском классе `TThread`. Код, находящийся внутри метода `Execute` будет исполняться в отдельном потоке.
При создании класса-наследника от `TThread` мы можем реализовать конструктор, деструктор, объявить собственные поля, методы, свойства и т.д., в зависимости от поставленной перед нами задачи.
В данном разделе я не буду подробно останавливаться на описании возможностей класса TThread. Читайте внимательно следующие разделы, надеюсь, в них Вы найдёте необходимую для Вас информацию.
:information_source: **Информация!** *Дополнительные потоки создаются с помощью вызова соответствующей функции операционной системы, например `CreateThread` в ОС Windows. Класс `TThread` является обёрткой, которая добавляет к системному дополнительному потоку объектно-ориентированное измерение, т.е. позволяет создать объект, который привязывается к системному дополнительному потоку.*
:information_source: **Информация!** Существуют и другие популярные средства для многопоточного программирования в Delphi. К ним стоит отнести:
\- **Parallel programming library** - стандартная библиотека для параллельного программирования в современных версиях Delphi.
\- **OmniThreadLibrary** – популярная библиотека для параллельного программирования (на июль 2020 библиотека поддерживает только Windows, однако в будущем может появиться поддержка других операционных систем). Ссылка: https://github.com/gabr42/OmniThreadLibrary
# 3. Предотвращаем зависание основного потока (имитация задачи длительного вычисления) <a name="app_hungup"></a>
Ниже представлен пример кода, в котором имитируется задача длительного вычисления (функция `DoLongCalculations`). Данная функция запускается двумя путями: 1) из основного потока (см. обработчик кнопки `btnRunInMainThread`); 2) из параллельного потока (см. обработчик кнопки `btnRunInParallelThread`). Данный пример Вы можете найти в репозитории (папка Ex1).
:information_source: **Информация!** *Все примеры к данному «Учебнику» доступны по ссылке*:
https://github.com/loginov-dmitry/multithread
```pascal
unit Ex1Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMyThread = class(TThread)
public
procedure Execute; override;
end;
TForm1 = class(TForm)
btnRunInParallelThread: TButton;
btnRunInMainThread: TButton;
procedure btnRunInParallelThreadClick(Sender: TObject);
procedure btnRunInMainThreadClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function DoLongCalculations: Int64;
var
I: Integer;
begin
Result := 0;
// Очень длинный цикл. Имитирует длительные вычисления.
for I := 1 to MaxInt do
Result := Result + Random(1000);
end;
procedure MyShowMessage(Msg: string);
begin
Windows.MessageBox(0, PChar(Msg), '', MB_OK);
end;
procedure TForm1.btnRunInParallelThreadClick(Sender: TObject);
begin
// Запускает параллельный поток
TMyThread.Create(False);
end;
{ TMyThread }
procedure TMyThread.Execute;
var
V: Int64;
begin
FreeOnTerminate := True;
V := DoLongCalculations;
MyShowMessage('Результат: ' + IntToStr(V));
end;
procedure TForm1.btnRunInMainThreadClick(Sender: TObject);
var
V: Int64;
begin
V := DoLongCalculations;
MyShowMessage('Результат: ' + IntToStr(V));
end;
end.
```
Нажмите кнопку `btnRunInMainThread` для запуска функции `DoLongCalculations` из основного потока. Через несколько секунд появится сообщение с результатом вычислений. Длительность вычислений предсказать сложно, она зависит от характеристик процессора, а также от его текущего состояния. Приблизительное время – от 5 до 20 секунд. Обратите внимание, что после запуска вычислений вы не можете выполнять каких-либо действий с окном (нажимать кнопки, изменять размеры окна и т.д.) до момента окончания вычислений. Таким образом, запуск функции `DoLongCalculations` из основного приводит к подвисанию интерфейса пользователя.
А теперь нажмите кнопку `btnRunInParallelThread` для запуска функции `DoLongCalculations` из дополнительного потока. Вы увидите, что интерфейс пользователя не подвис, а продолжает реагировать на Ваши действия. Через несколько секунд появится сообщение с результатом вычислений. Таким образом, запуск функции `DoLongCalculations` из параллельного потока не приводит к подвисанию интерфейса пользователя.
Попробуйте нажать кнопку `btnRunInParallelThread` два раза друг за другом. Через несколько секунд (скорее всего дольше, чем до этого) появится два сообщения с результатами вычислений. Т.е. параллельно друг другу два дополнительных потока произведут вычисления, а затем выведут результат на экран.
Сложно предположить, сколько времени займут вычисления при выполнении двух параллельных потоков. Это зависит от того, сколько **физических** ядер у процессора, а также от того, на одном или на двух разных физических ядрах выполняются потоки. Если наши два потока будут выполняться на двух разных физических ядрах, то время их выполнения будет таким же, как время выполнения одного потока, который мы запускали до этого. Но если они будут выполняться на одном физическом ядре, то время их выполнения будет примерно в 2 или 3 раза больше (т.е. мы не получим реального распараллеливания вычислений, хотя интерфейс пользователя и не будет подвисать).
Я не случайно выделил слово «физических», поскольку в современных процессорах есть понятие «логическое ядро» (можете почитать в Интернете про «Hyper-Threading»). Если оба потока будут работать на двух логических ядрах одного и того же физического ядра, то мы также не получим реального распараллеливания вычислений.
:warning: **Замечание по выдаче окна с сообщением из дополнительного потока**
В данном примере для выдачи пользователю сообщения из дополнительного потока используется функция `MyShowMessage`. В ней производится вызов стандартной WinAPI-функции `MessageBox`, которая отображает на экране окно с заданным текстовым сообщением. Хотя в Delphi и имеется похожая функция для вывода сообщений – `ShowMessage`, однако её нельзя вызывать из дополнительного потока, поскольку она не является «потокобезопасной», т.е. при обращении к ней из дополнительного потока могут происходить сбои в работе программы (может и не каждый раз).
Несмотря на то, что в данном примере код параллельного потока выдаёт пользователю окно с сообщением, я не рекомендую так делать в реальных программах. Важно помнить, что визуальное информирование пользователя должно выполняться из основного потока, тогда мы сможем избежать многих проблем.
:warning: **Замечания по использованию свойства TThread.FreeOnTerminate**
Обратите внимание, что в примере используется свойство `FreeOnTerminate` класса `TThread`. В начале метода `TMyThread.Execute` мы выставили ему значение True. Сделано этого для того, чтобы программа автоматически уничтожила объект `TMyThread` (т.е. вызвала деструктор объекта) сразу после завершения работы метода `Execute`. Поскольку мы не завели отдельной переменной для хранения ссылки на созданный объект `TMyThread`, то единственный способ уничтожить созданный объект – попросить его самостоятельно себя уничтожить сразу после завершения работы метода `Execute`. Для этого мы и воспользовались свойством `FreeOnTerminate`.
Это лишь один из способов управления временем жизни объекта потока. Другие способы будет представлены в следующих разделах.
:warning: **Запущенный поток не завершается при выходе из программы**
В данном примере есть следующая проблема: мы можем закрыть программу, не дожидаясь окончания работы дополнительного потока. Хотя для первого примера это не является проблемой, однако в реальных программах такая ситуация является недопустимой. Запомните правило: при выходе из программы Вы должны корректно завершать все созданные Вами дополнительные потоки. Можно сказать по-другому: нельзя допустить завершения работы программы, если имеются запущенные Вами дополнительные потоки. Если Вы не будете следовать этому правилу, то Ваша программа с большой вероятностью будет глючить.
# 4. Управление временем жизни потоков <a name="thread_lifetime_control"></a>
При программировании на языке Delphi программист должен самостоятельно думать о том, каким образом будет уничтожаться тот или иной созданный им объект.
При программировании дополнительных потоков мы должны определиться, каким образом будет завершаться работа метода `Execute` и каким образом будет уничтожаться созданный нами объект потока. При этом мы должны гарантированно завершить работу потока и уничтожить объект потока при выходе из программы. Если поток выполняет очень длительную важную задачу и его нельзя прерывать, то мы должны заблокировать возможность выхода из программы до момента завершения работы потока.
В зависимости от задачи, которую решает дополнительный поток, код в методе `Execute` может выполняться однократно либо многократно.
Примеры однократного выполнения кода в методе `Execute`:
1) Выполнение вычислений при нажатии пользователем кнопки. В этом случае вполне уместно при каждом нажатии пользователем кнопки создавать дополнительный поток и запускать его.
2) Выполнение запроса к базе данных и обработка данных при срабатывании таймера. Здесь следует пояснить, что таймер не должен срабатывать слишком часто, иначе расходы на запуск и уничтожение потока могут оказаться значительными. Также хочу отметить, что при срабатывании таймера не следует обращаться к базе данных из основного потока, лучше это делать из дополнительного потока, разумеется, в отдельном подключении.
3) Выполнение заданной функции или процедуры в контексте дополнительного потока с навешиванием модальной формы ожидания окончания выполнения заданной функции.
4) Можно придумать ещё сотню различных ситуаций, но такой необходимости у нас нет!
Многократное выполнение кода в методе `Execute` используется при периодическом выполнении какой-либо задачи, например:
1) Раз в секунду выполняется запрос к БД и при наличии нового задания производится обмен с каким-либо устройством.
2) Раз в 15 секунд выполняется синхронизация данных с сервером.
3) 2 раза в секунду выполняется опрос состояния какого-либо устройства (например, фискального регистратора, топливно-раздаточной колонки, системы измерения уровня и т.д.).
4) Можно также придумать ещё сотню различных ситуаций.
:information_source: **Внимание!** *Если периодическая задача выполняется редко (например, каждые 10 минут), рекомендуется каждый раз (если это не сложно!) для такой задачи создавать новый поток. Вероятно, это лучше, чем часами удерживать дополнительный поток в спящем состоянии (особенно, если вы разрабатываете 32-разрядное Windows-приложение).*
## 4.1 Использование свойства TThread.Terminated для выхода из метода Execute <a name="use_terminated"></a>
В том случае, если поток запущен на длительное время (например, пока пользователь не закроет программу) и периодически выполняет одну и ту же задачу, необходимо предусмотреть механизм завершения работы метода `Execute` (работа потока завершается именно после выхода из метода `Execute`). Код внутри метода `Execute` должен периодически проверять флаг (например, переменную логического типа), который программа должна установить для того, чтобы поток знал, что пора завершаться.
Свойство `TThread.Terminated` как раз и является тем флагом, который необходимо периодически проверять для завершения работы метода `Execute`. Ниже представлен пример реализации метода `Execute`, в котором анализируется значения свойства `Terminated` (см. папку Ex2).
```pascal
procedure TMyLongThread.Execute;
procedure WaitTimeout(ATimeOut: Integer);
begin
Sleep(ATimeOut);
end;
begin
while not Terminated do
begin
DoUsefullTask;
WaitTimeout(10000); // Ожидаем таймаут 10 сек.
end;
end;
```
В данном примере реализован бесконечный цикл `while`, критерием выхода из которого является значение свойства `Terminated`, равное `True`.
Метод `DoUsefullTask` имитирует выполнение полезной работы, которая длится 5 секунд.
После каждой итерации выполнения полезной работы поток должен на какое-то время переходить в спящее состояние. Напомню, что в спящем состоянии поток не создаёт какой-либо нагрузки на процессор. Благодаря этому мы даём возможность работать другим потокам (в том числе в других процессах).
Для перевода потока в спящее состояние мы вызываем вложенную (nested) процедуру `WaitTimeout` и передаём ей длительность ожидания. В свою очередь в ней происходит вызов стандартной WinAPI-функции `Sleep`, после чего поток переходит в спящее состояние, длительность которого задана в миллисекундах (в примере – 10 секунд).
:warning: **Внимание!** *Если поток переведён в спящее состояние с помощью функции `Sleep`, то не существует другого способа выйти из этого состояния кроме истечения указанного временного периода.*
Попробуйте в примере (Ex2) запустить параллельный поток (с помощью кнопки `btnRunParallelThread`), а затем закрыть программу. Вы обнаружите, что выход из программы произойдет не сразу, а спустя какое-то время (максимум 15 секунд). На практике вызов функции Sleep для длительных задержек является недопустимым!
## 4.2 Главная форма отвечает за прекращение работы потока и уничтожение объекта потока <a name="mainform_is_owner"></a>
Ниже приведён полный пример из каталога Ex2. В нём, как уже было сказано, код в методе `Execute` в бесконечном цикле выполняет заданную задачу и засыпает на некоторое время. Работа метода `Execute` завершается только при выходе из программы:
```pascal
unit Ex2Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMyLongThread = class(TThread)
private
procedure DoUsefullTask; // Процедура для имитации полезной работы
public
procedure Execute; override;
end;
TForm1 = class(TForm)
btnRunParallelThread: TButton;
procedure btnRunParallelThreadClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
MyThread: TMyLongThread;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnRunParallelThreadClick(Sender: TObject);
begin
// Запускает параллельный поток
if MyThread = nil then
MyThread := TMyLongThread.Create(False)
else
raise Exception.Create('Дополнительный поток уже запущен!');
end;
{ TMyLongThread }
procedure TMyLongThread.DoUsefullTask;
begin
// Реальный поток может выполнять какую угодно полезную работу
// В учебных целях делаем паузу 5 секунд для имитации задержки, которая
// может возникнуть при выполнении полезной работы
Sleep(5000);
end;
procedure TMyLongThread.Execute;
procedure WaitTimeout(ATimeOut: Integer);
begin
Sleep(ATimeOut);
end;
begin
while not Terminated do
begin
DoUsefullTask;
WaitTimeout(10000); // Ожидаем таймаут 10 сек.
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
// При закрытии программы необходимо завершить работу потока
// и уничтожить объект потока MyThread
if MyThread <> nil then
MyThread.Free;
end;
end.
```
Обратите внимание на следующие особенности данного примера:
1) не используется свойство `FreeOnTerminate`, т.е. объект потока не уничтожает себя автоматически после завершения метода `Execute`;
2) при создании объекта потока ссылка на созданный объект запоминается в переменной `MyThread`, которая является полем класса `TForm1`;
3) блокируется создание нескольких объектов класса `TMyLongThread`;
4) завершение работы потока и уничтожение объекта потока выполняется при выходе из программы путём вызова стандартного метода `MyThread.Free` (в обработчике `FormDestroy`). *Хотя технически и нет необходимости проверять переменную `MyThread` на равенство `nil` перед вызовом метода `Free`, однако в данном случае проверка на `nil` делает код более наглядным, поскольку программа не обязывает пользователя нажимать кнопку `btnRunParallelThread`.*
Важно понимать хотя бы приблизительно, что происходит при вызове `MyThread.Free`. А происходит примерно следующее:
1) Производится вызов метода `TThread.Terminate`, который выставляет свойству `TThread.Terminated` значение `True`.
2) Главный поток (т.е. поток, управляющий интерфейсом пользователя) переводится в спящее состояние до тех пор, пока в дополнительном потоке не произойдет выход из метода `Execute`.
3) В методе `Execute` класса `TMyLongThread` рано или поздно произойдёт проверка значения свойства `Terminated` и бесконечный цикл `while` прервётся, а после этого произойдёт выход из метода `Execute` и работа параллельного потока будет завершена.
4) Главный поток вновь возобновит свою работу и окончательно уничтожит объект потока `MyThread`.
:information_source: **Совет!** *Старайтесь и Вы сохранять ссылки на создаваемые потоки и уничтожать объекты потоков при закрытии (или уничтожении) форм, в которых Вы создавали соответствующий поток!*
## 4.3 Главная форма отвечает за уничтожение объекта потока с разовой задачей <a name="mainform_is_owner_short_thread"></a>
Следующий пример (Ex3) интересен тем, что дополнительный поток выполняет разовую задачу длительностью 5 секунд. Обратите внимание, что после завершения работы дополнительного потока объект потока `MyThread` всё ещё жив, и жить он будет до тех пор, пока не будет произведён выход из программы. Также в примере добавлена визуализация работы дополнительного потока с помощью модуля `ProgressViewer.pas`. Я его иногда использую в некоторых рабочих проектах, но лицензия не запрещает его использовать всём желающим. Думаю, что так будет интереснее наблюдать за работой потоков.
```pascal
unit Ex3Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ProgressViewer;
type
TMyShortThread = class(TThread)
private
procedure DoUsefullTask; // Процедура для имитации полезной работы
public
procedure Execute; override;
end;
TForm1 = class(TForm)
btnRunParallelThread: TButton;
procedure btnRunParallelThreadClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
MyThread: TMyShortThread;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnRunParallelThreadClick(Sender: TObject);
begin
// Запускает параллельный поток. Если объект потока уже создан,
// то уничтожает его.
if MyThread <> nil then
FreeAndNil(MyThread);
MyThread := TMyShortThread.Create(False);
end;
{ TMyShortThread }
procedure TMyShortThread.DoUsefullTask;
var
AProgress: TProgressViewer;
begin
// Реальный поток может выполнять какую угодно полезную работу
// В учебных целях делаем паузу 5 секунд для имитации задержки, которая
// может возникнуть при выполнении полезной работы
AProgress := TProgressViewer.Create('Выполняется поток TMyShortThread');
Sleep(5000);
AProgress.TerminateProgress;
end;
procedure TMyShortThread.Execute;
begin
DoUsefullTask;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
// При закрытии программы необходимо завершить работу потока
// и уничтожить объект потока MyThread
if MyThread <> nil then
MyThread.Free;
end;
end.
```
Хотелось бы отменить ещё некоторые особенности этого примера:
1) При нажатии кнопки `btnRunParallelThread` программа уничтожает объект потока, если он был запущен ранее. Предлагаю поэкспериментировать с этой кнопкой – понажимать её несколько раз. Вы обнаружите, что если окно с сообщением «Выполняется поток TMyShortThread» отображается на экране, то нажатие кнопки `btnRunParallelThread` приводит к подвисанию интерфейса пользователя до тех пор, пока окно с сообщением не исчезнет. Затем окно с сообщением появится ещё раз, и отсчёт времени начнётся с нуля.
2) При выходе из программы возможны 3 варианта:
- Вариант 1: пользователь не нажимал кнопку `btnRunParallelThread`, поэтому объект потока не создан и ссылка `MyThread` равна `nil`. В этом случае программа гарантированно закроется без задержки.
- Вариант 2: пользователь нажал кнопку `btnRunParallelThread` и сразу, не дожидаясь окончания работы потока (т.е. сообщение «Выполняется поток TMyShortThread» ещё висело на экране), закрыл программу. В этом случае произойдет подвисание интерфейса пользователя (максимум 5 секунд), затем, после завершения работы дополнительного потока, произойдет уничтожение объекта потока, после чего программа закроется.
- Вариант 3: пользователь нажал кнопку `btnRunParallelThread`, подождал более 5 секунд (сообщение «Выполняется поток TMyShortThread» исчезло) и закрыл программу. В этом случае произойдет вызов метода `MyThread.Free`, который просто уничтожит объект потока, который к этому моменту уже завершил свою работу, а после этого программа закроется.
## 4.4 Главная форма отвечает за уничтожение нескольких долгоживущих потоков <a name="mainform_is_owner_some_thread"></a>
Мы уже разобрались, как правильно завершить работу одного долгоживущего потока при выходе из программы. А как быть, если таких потоков несколько (например, два или три), а мы хотим максимально ускорить закрытие программы? В этом случае мы должны заранее сообщить каждому потоку о необходимости завершения их работы, а уничтожение объектов потоков выполнить в последнюю очередь. Демонстрация представлена в следующем примере (Ex4):
```pascal
unit Ex4Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ProgressViewer;
type
TMyLongThread = class(TThread)
private
FTaskNum: Integer;
procedure DoUsefullTask1; // Первая задача
procedure DoUsefullTask2; // Вторая задача
procedure DoFinalizeTask; // Задача запускается при завершении работы потока
public
constructor Create(TaskNum: Integer);
procedure Execute; override;
end;
TForm1 = class(TForm)
btnRunParallelThreads: TButton;
Label1: TLabel;
cbTerminateMode: TComboBox;
procedure btnRunParallelThreadsClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
MyThread1: TMyLongThread; // Поток для первой задачи
MyThread2: TMyLongThread; // Поток для второй задачи
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnRunParallelThreadsClick(Sender: TObject);
begin
// Запускает параллельный поток для задачи 1
if MyThread1 = nil then
MyThread1 := TMyLongThread.Create(1);
// Запускает параллельный поток для задачи 2
if MyThread2 = nil then
MyThread2 := TMyLongThread.Create(2);
end;
{ TMyLongThread }
constructor TMyLongThread.Create(TaskNum: Integer);
begin
inherited Create(False); // Вызываем родительский конструктор
// Запоминаем параметр TaskNum. Он нужен в методе Execute
FTaskNum := TaskNum;
end;
procedure TMyLongThread.DoFinalizeTask;
begin
Sleep(5000); // Данная условная задача занимает 5 секунд
end;
procedure TMyLongThread.DoUsefullTask1;
begin
Sleep(1000); // Данная условная задача занимает 1 секунду
end;
procedure TMyLongThread.DoUsefullTask2;
begin
Sleep(2000); // Данная условная задача занимает 2 секунды
end;
procedure TMyLongThread.Execute;
procedure WaitTimeout(ATimeOut: Integer);
begin
Sleep(ATimeOut);
end;
begin
while True do
begin
if Terminated then
begin
DoFinalizeTask; // Некоторые действия при завершении потока
Exit; // Завершаем работу потока
end else
begin
if FTaskNum = 1 then
DoUsefullTask1 // Запускаем задачу 1
else
DoUsefullTask2; // Запускаем задачу 2
if not Terminated then // Дополнительная проверка не повредит!
WaitTimeout(1000); // Ожидаем таймаут 1 сек
end;
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
var
AProgress: TProgressViewer;
begin
AProgress := TProgressViewer.Create('Выход из программы');
try
if cbTerminateMode.ItemIndex = 1 then
begin // Выбран режим "Одновременно (быстрее)"
if Assigned(MyThread1) then
MyThread1.Terminate; // Выставляем флаг Terminated
if Assigned(MyThread2) then
MyThread2.Terminate; // Выставляем флаг Terminated
end;
MyThread1.Free;
MyThread2.Free;
finally
AProgress.TerminateProgress;
end;
end;
end.
```
В этом примере имеются следующие особенности:
1) Поток `TMyLongThread` выполняет периодически указанное действие: `DoUsefullTask1` либо `DoUsefullTask2`. Номер действия (`TaskNum`) указан при создании потока, например:
`MyThread1 := TMyLongThread.Create(1);`
`MyThread2 := TMyLongThread.Create(2);`
2) При нажатии кнопки `btnRunParallelThreads` создаются два потока: `MyThread1` (для решения задачи 1) и `MyThread2` (для решения задачи 2). Для обоих потоков используется один и тот же класс `TMyLongThread`.
3) В классе TMyLongThread объявлен собственный конструктор Create.
:exclamation: **Важно!** Обратите внимание, что при реализации собственного конструктора мы должны обязательно вызвать родительский конструктор, например: `inherited Create(False)`!
При вызове конструктора `TThread.Create` требуется указать логическое значение (параметр `CreateSuspended`). Мы указали значение `False`. Это значит, что мы не хотим запускать поток в замороженном (`Suspended`) состоянии. Наш поток должен запускаться сразу, как только отработает конструктор. Если бы мы создавали поток в замороженном состоянии, то для запуска метода Execute нам пришлось бы вызвать метод `TThread.Resume` либо его современную версию `TThread.Start`. Такая необходимость возникает редко (по крайней мере, в моих проектах).
:exclamation: **Важно!** Вы должны знать, что запуск метода Execute в дополнительном потоке произойдет только после того, как работа конструктора полностью завершится!
4) Логика бесконечного цикла `while` в методе `Execute` изменена. Если флаг `Terminated` выставлен в `True`, то выполняются некоторые действия (метод `DoFinalizeTask`) и производится выход из метода `Execute`. Если же флаг `Terminated` имеет значение `False`, то осуществляется вызов метода `DoUsefullTask1` либо `DoUsefullTask2`, в зависимости от переменной `FTaskNum`. Далее, перед вызовом функции `WaitTimeout`, производится дополнительная проверка флага `Terminated`. Если он выставлен, то не нужно тратить лишнее время на ожидание, вместо этого мы должны как можно быстрее завершить работу потока!
5) В программе пользователь может выбрать один из способов завершения работы потоков: «последовательно» или «одновременно». Если выбрать вариант «последовательно» и закрыть программу, то в методе `TForm1.FormDestroy` будет произведён сначала вызов `MyThread1.Free`, а затем вызов `MyThread2.Free`. В том случае, если пользователь предварительно запустил потоки, мы увидим окно с текстом «Выход из программы», которое будет отображаться примерно 10 секунд.
Если же мы выберем вариант «одновременно» и закроем программу, то окно с текстом «Выход из программы», будет отображаться примерно 5 секунд. Это происходит благодаря тому, что перед вызовом `MyThread1.Free` осуществляется уведомление потоков о необходимости завершения их работы путем вызова метода `TThread.Terminate` (`MyThread1.Terminate` и `MyThread2.Terminate`). При вызове `MyThread1.Free` ожидается завершение потока `MyThread1`, а затем выполняется уничтожение объекта потока. При вызове `MyThread2.Free` ожидать завершения второго потока долго не придётся, т.к. завершаться он начал одновременно с первым потоком после вызова `MyThread2.Terminate`.
:information_source: **Совет!** *Старайтесь при выходе из программы заранее вызывать метод `TThread.Terminate` для каждого из запущенных потоков!*
:warning: **Внимание!** Для данного примера перед вызовом метода `Terminate` необходимо проверять ссылку на объект потока. Если в ней содержится значение `nil`, то вызывать метод `Terminate` нельзя, т.к. это приведёт к ошибке доступа к памяти! Также нельзя обращаться к полям, свойствам и методам объекта, который не создан! Единственный метод, который допускает свой вызов для несозданного объекта: `TObject.Free`. В нём проверяется значение ссылки на объект и если там `nil`, то деструктор не вызывается.
## 4.5 Использование списка TObjectList для хранения ссылок на объекты потоков <a name="use_object_list"></a>
В том случае, если в Вашем приложении множество потоков, каждый из которых выполняет различные задачи, Вы можете использовать список `TObjectList` для хранения ссылок на создаваемые объекты потоков. В таком случае Вам не придётся писать код уничтожения каждого потока, достаточно вызвать метод `Free` для списка `TObjectList`.
В следующем примере (Ex5) демонстрируется работа со списком `TObjectList`. Как и в предыдущем примере, в нём реализовано 2 режима завершения работы потоков при выходе из программы: последовательный и одновременный.
Ниже представлен исходный код модуля `Ex5Unit`:
```pascal
unit Ex5Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ProgressViewer, Contnrs, MTUtils;
type
TMyLongThread1 = class(TThread)
private
FUsefullTaskTime: Integer;
public
constructor Create(UsefullTaskTime: Integer);
procedure Execute; override;
end;
TMyLongThread2 = class(TThread)
public
procedure Execute; override;
end;
TMyLongThread3 = class(TThread)
private
FUsefullTaskTime: Integer;
public
constructor Create(UsefullTaskTime: Integer);
procedure Execute; override;
end;
TMyLongThread4 = class(TThread)
public
procedure Execute; override;
end;
TForm1 = class(TForm)
btnRunParallelThreads: TButton;
Label1: TLabel;
cbTerminateMode: TComboBox;
procedure btnRunParallelThreadsClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
FList: TObjectList; // Потоки для первой и второй задачи
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnRunParallelThreadsClick(Sender: TObject);
begin
// Запускаем 4 параллельных потока
if FList.Count = 0 then
begin
FList.Add(TMyLongThread1.Create(1000));
FList.Add(TMyLongThread2.Create(False));
FList.Add(TMyLongThread3.Create(2000));
FList.Add(TMyLongThread4.Create(False));
end;
end;
{ TMyLongThread }
constructor TMyLongThread1.Create(UsefullTaskTime: Integer);
begin
inherited Create(False); // Вызываем родительский конструктор
FUsefullTaskTime := UsefullTaskTime;
end;
procedure TMyLongThread1.Execute;
begin
while not Terminated do
begin
EmulateUsefullWork(FUsefullTaskTime);
ThreadWaitTimeout(Self, 60000); // Ожидаем таймаут 60 сек
end;
Sleep(5000); // Оставлено для демонстрации режима "Одновременно"
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FList := TObjectList.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
var
AProgress: TProgressViewer;
I: Integer;
begin
AProgress := TProgressViewer.Create('Выход из программы');
try
if cbTerminateMode.ItemIndex = 1 then
begin // Выбран режим "Одновременно (быстрее)"
// Выставляем флаг Terminated для всех потоков. Можно использовать
// родительский класс TThread для операции приведения типов.
for I := 0 to FList.Count - 1 do
TThread(FList[I]).Terminate;
end;
// При уничтожении списка TObjectList будут уничтожены все объекты потоков
FList.Free;
finally
AProgress.TerminateProgress;
end;
end;
{ TMyLongThread2 }
procedure TMyLongThread2.Execute;
begin
while not Terminated do
begin
EmulateUsefullWork(2000);
ThreadWaitTimeout(Self, 60000); // Ожидаем таймаут 60 сек
end;
Sleep(5000); // Оставлено для демонстрации режима "Одновременно"
end;
{ TMyLongThread3 }
constructor TMyLongThread3.Create(UsefullTaskTime: Integer);
begin
inherited Create(False);
FUsefullTaskTime := UsefullTaskTime;
end;
procedure TMyLongThread3.Execute;
begin
while not Terminated do
begin
EmulateUsefullWork(FUsefullTaskTime);
ThreadWaitTimeout(Self, 60000); // Ожидаем таймаут 60 сек
end;
Sleep(5000); // Оставлено для демонстрации режима "Одновременно"
end;
{ TMyLongThread4 }
procedure TMyLongThread4.Execute;
begin
while not Terminated do
begin
EmulateUsefullWork(1000);
ThreadWaitTimeout(Self, 60000); // Ожидаем таймаут 60 сек
end;
Sleep(5000); // Оставлено для демонстрации режима "Одновременно"
end;
end.
```
Обратите внимание на следующие особенности данного примера:
1) Реализованы 4 класса-наследника от `TThread`: `TMyLongThread1`, `TMyLongThread2`, `TMyLongThread3`, `TMyLongThread4`. Это сделано для того, чтобы показать читателю, что в список `TObjectList` можно добавить произвольные объекты потоков и в любом количестве.
2) Список `FList` создаётся в конструкторе формы и уничтожается в деструкторе. Класс `TObjectList` объявлен в модуле «`Contnrs`» (вечно забываю, какие буквы в этом слове и приходится пользоваться поиском по исходникам Delphi).
3) Для организации паузы в работе потоков используется простая и удобная функция `ThreadWaitTimeout`. из модуля `MTUtils.pas`. Она будет приведена ниже.
4) Для эмуляции полезной работы потоков используется функция `EmulateUsefullWork` из модуля `MTUtils.pas`. Она состоит из одной строчки: `Sleep(WorkTime)`.
5) В режиме одновременного завершения работы потоков в методе `TForm1.FormDestroy` в цикле выставляется значение `True` свойству `Terminated` для каждого объекта-потока из списка `FList`.
6) При создании объектов-потоков (в обработчике `TForm1.btnRunParallelThreadsClick`) переменные для объектов-потоков не объявляются. Результат, возвращаемый при создании объекта (например, `TMyLongThread1.Create(1000)`), можно сразу сохранять в списке `FList`.
## 4.6 Простой пример организации паузы в работе потока <a name="thread_pause"></a>
Ниже приведён код функции `ThreadWaitTimeout` из модуля `MTUtils.pas`:
```pascal
procedure ThreadWaitTimeout(AThread: TThread; ATimeout: Integer);
var
p: TTimeInterval;
T: TThreadAccessTerminated;
begin
// Получаем доступ к protected-свойству Terminated
T := TThreadAccessTerminated(AThread);
// Если поток нужно завершить, то сразу выходим из цикла
if T.Terminated then Exit;
p.Start; // Начинаем замер времени
while True do
begin
if T.Terminated or (p.ElapsedMilliseconds >= ATimeout) then
Exit;
// Замораживаем поток примерно на 20 мс
Sleep(ThreadWaitTimeoutSleepTime);
end;
end;
```
Организация паузы в этой функции основана на циклическом вызове функции `Sleep` с небольшим таймаутом (20 мс). При этом контролируется свойство `TThread.Terminated`. Опытные программисты могут использовать более сложные способы организации паузы, например, с использованием объекта синхронизации «Event». Однако на текущем этапе обучения поднимать тему объектов синхронизации считаю преждевременным. Для большинства практических задач Вам будет достаточно функции `ThreadWaitTimeout`. Задержку в 20 мс считаю оптимальной, она обеспечивает достаточную точность паузы, но при этом не приводит к слишком частым переключениям контекста, т.е. практически не оказывает нагрузки на процессор. При необходимости Вы можете присвоить глобальной переменной `ThreadWaitTimeoutSleepTime` другое значение, отличное от 20.
Также обратите внимание, что в этой функции для организации замеров промежутков времени используется структура (record) `TTimeInterval`, объявленная в модуле `TimeIntervals.pas`. Вы можете свободно использовать модуль `TimeIntervals.pas` в собственных проектах. Обсуждение данного модуля ведётся на форуме [SQL.ru](https://www.sql.ru/forum/1326896/modul-dlya-udobnogo-zamera-tochnyh-intervalov-vremeni)
## 4.7 Использование свойства FreeOnTerminate для автоматического уничтожения объекта потока при завершении работы потока <a name="free_on_terminate"></a>
Давайте снова рассмотрим первый пример (Ex1). В нём был показан вариант реализации потока с использованием свойства `FreeOnTerminate`. Как уже было отмечено ранее, при значении свойства `FreeOnTerminate=True` объект потока уничтожается автоматически, как только завершается работа потока. Вроде удобно, да? Не требуется заводить переменную для объекта-потока, не нужно писать вызов метода `Free` для уничтожения объекта потока. Однако тут не всё так просто! Я бы даже сказал, что у такого подхода больше минусов, чем плюсов. Отмечу такие минусы:
1) Сложно контролировать состояние такого потока, поскольку не объявлена переменная для объекта-потока.
2) Сложно управлять таким потоком (по той же причине).
3) Если же мы решим объявить для такого потока переменную и будем пытаться с нею работать, то рискуем нарваться на ошибку доступа к памяти «Access Violation», поскольку объект потока может быть уничтожен в любой момент времени.
4) Сложно организовать корректный выход из программы. Как уже было сказано ранее, при выходе из программы мы должны гарантировать завершение работы всех запущенных нами потоков. Но это сложно сделать, если у нас нет ссылки на объект потока.
В первом примере было бы лучше использовать список `TObjectList` для хранения ссылок на созданные объекты потоков. Однако такое усложнение могло бы отпугнуть читателя на этапе знакомства с первым примером.
Вывод такой: я не рекомендую использовать данный режим уничтожения объектов потоков (особенно для новичков). Если Вы по каким-то причинам решили его использовать, то следует проявлять осторожность и предусмотреть механизмы контроля состояния потока и корректного выхода из программы.
## 4.8 Организация корректного завершения работы программы при использовании свойства FreeOnTerminate <a name="free_on_terminate_correct_exit"></a>
В следующем примере (Ex6) демонстрируется один из подходов к организации корректного выхода из программы при использовании свойства `FreeOnTerminate`. Ниже представлен код модуля `Ex6Unit`:
```pascal
unit Ex6Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ProgressViewer, MTUtils , TimeIntervals;
type
TMyThread = class(TThread)
private
FThreadNum: Integer;
public
procedure Execute; override;
constructor Create;
destructor Destroy; override;
end;
TForm1 = class(TForm)
btnRunInParallelThread: TButton;
procedure btnRunInParallelThreadClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
ThreadCount: Integer;
StopThreadsFlag: Boolean;
implementation
{$R *.dfm}
procedure TForm1.btnRunInParallelThreadClick(Sender: TObject);
begin
// Запускаем параллельный поток
TMyThread.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
var
pv: TProgressViewer;
begin
// Выставляем флаг StopThreadsFlag, чтобы все потоки завершились
StopThreadsFlag := True;
// Задерживаем выход из программы, пока не будут завершены все потоки
if ThreadCount > 0 then
begin
pv := TProgressViewer.Create('Ожидаем завершение потоков');
while ThreadCount > 0 do
Sleep(10);
pv.TerminateProgress;
end;
end;
{ TMyThread }
constructor TMyThread.Create;
begin
inherited Create(False);
// Увеличиваем глобальную переменную ThreadCount на 1 и запоминаем
// полученное значение
FThreadNum := InterlockedIncrement(ThreadCount);
end;
destructor TMyThread.Destroy;
begin
inherited;
// Уменьшаем глобальную переменную ThreadCount на 1
InterlockedDecrement(ThreadCount);
end;
procedure TMyThread.Execute;
var
ti: TTimeInterval;
begin
FreeOnTerminate := True;
// Организуем паузу 10 секунд. При этом каждые 20 мс
// проверяем флаг StopThreadsFlag
ti.Start;
while ti.ElapsedSeconds < 10 do
begin
// Заканчиваем ожидание, если выставлен флаг StopThreadsFlag
if StopThreadsFlag then Break;
Sleep(20);
end;
ThreadShowMessageFmt('Работа потока #%d завершена!', [FThreadNum]);
end;
end.
```
Особенности данного примера:
1) Введены две глобальные переменные: `ThreadCount` (количество запущенных потоков) и `StopThreadsFlag` (флаг завершения потоков).
2) Для увеличения переменной `ThreadCount` на единицу используется потокобезопасная функция `InterlockedIncrement`. В принципе, в этом примере мы могли бы использовать операцию инкремента `Inc` (либо `ThreadCount := ThreadCount + 1`), однако, операция `Inc` не является потокобезопасной и при одновременном вызове `Inc(ThreadCount)` из нескольких потоков мы могли бы получить некорректное значение (например, 9 вместо 10).
3) Каждому запущенному потоку присваивается порядковый номер `FThreadNum`. Для этого используется результат, который возвращает функция `InterlockedIncrement`. Благодаря функции `InterlockedIncrement` гарантируется, что каждый поток получит свой уникальный порядковый номер. Если бы мы использовали следующий код для увеличения `ThreadCount` и запоминания `FThreadNum`:
`ThreadCount := ThreadCount + 1; // либо Inc(ThreadCount)`
`FThreadNum := ThreadCount;`
то столкнулись бы со следующими проблемами (с вероятностью примерно 1%):
а) при одновременном вызове `ThreadCount := ThreadCount + 1` из нескольких потоков мы можем получить некорректное значение (например, 9 вместо 10). Тут есть очень простое объяснение. Допустим, 2 потока начали выполнять команду инкремента одновременно, в этом момент в переменной `ThreadCount` хранилось значение 8. Далее оба потока одновременно увеличили это значение на 1, получилось значение 9. Функция `InterlockedIncrement` такого не допустит!
б) даже если инкремент `ThreadCount` был выполнен корректно, существует риск того, что в поле `FThreadNum` нескольких потоков попадёт одно и то же значение. Причина этого может заключаться в том, что планировщик задач Windows может в любой момент приостановить работу потока, например, непосредственно перед инструкцией `FThreadNum := ThreadCount`. После того, как планировщик возобновит работу потока, в переменной ThreadCount может оказаться совсем другое значение. Функция `InterlockedIncrement` такого не допустит!
4) Для уменьшения переменной `ThreadCount` на единицу используется потокобезопасная функция `InterlockedDecrement` в методе-деструкторе `TMyThread.Destroy`.
5) Для остановки потоков используется глобальная переменная `StopThreadsFlag`. Ей присваивается значение `True` в методе `TForm1.FormDestroy`. Переменная `StopThreadsFlag` проверяется в цикле в методе `TMyThread.Execute`. Цикл прерывается после того, как в переменной `StopThreadsFlag` окажется значение `True`. Перед выходом из метода `TMyThread.Execute` выдаётся сообщение с текстом «Работа потока #%d завершена!». Для вывода сообщения используется функция `ThreadShowMessageFmt`, объявленная в модуле `MTUtils`. Также обратите внимание, что деструктор будет вызван после выхода из метода `TMyThread.Execute`!
6) Для организации цикла ожидания 10 секунд в методе `TMyThread.Execute` используется тип `TTimeInterval`, объявленный в модуле `TimeIntervals`. Обратите внимание, что поддержка методов в записях появилась в версии Delphi 2007, поэтому в более ранних версиях Delphi примеры не скомпилируются.
7) При выходе из приложения в методе `TForm1.FormDestroy` выставляется флаг `StopThreadsFlag := True`, а затем выполняется цикл ожидания нулевого значения в переменной `ThreadCount`. Данная переменная проверяется каждые 10 мс (пауза в работе основного потока организована с помощью `Sleep(10)`). Выход из программы произойдет после того, как пользователь нажмёт кнопку «ОК», во всех сообщениях, открытых из дополнительных потоков.
8) Кнопка `btnRunInParallelThread` может быть нажата произвольное количество раз.
:warning: **Внимание!** *Функции `InterlockedIncrement` и `InterlockedDecrement` являются частью Windows API. В современных версиях Delphi Вы можете использовать их кроссплатформенные аналоги: `AtomicIncrement` и `AtomicDecrement`*.
# 5. Передача информации из дополнительного потока в основной <a name="send_info_to_main_thread"></a>
При разработке многопоточных приложений в Delphi очень актуальной является задача передачи информации, подготовленной в дополнительном потоке, в главный поток. Это весьма сложная тема, требующая от Delphi-программиста повышенного внимания, т.к. при недостаточных знаниях у программиста есть очень высокий шанс сделать программу глюченной и нестабильной.
## 5.1 Обращение к визуальным компонентам формы из дополнительного потока – как нельзя делать <a name="vcl_not_threadsafe"></a>
:exclamation: **Внимание!** *Не допускается обращение к визуальным компонентам из дополнительного потока!*
Т.е. внутри метода `Execute` мы не можем просто так взять, и изменить что-либо на форме. Например, следующие инструкции внутри метода `Execute` недопустимы:
```pascal
Form1.Label1 := 'Привет';
Form1.Show;
Form1.ShowModal;
Form1.Button1.Visible := True;
Form1.Memo1.Lines.Add('Привет');
и т.д.
```
Запомните, что интерфейс пользователя обслуживается основным (главным) потоком! Любые изменения, влияющие на интерфейс пользователя должны всегда выполняться в контексте основного потока! Это не является уникальной особенностью Delphi. Для других языков программирования, в которых имеется поддержка разработки визуальных интерфейсов, действует такое же правило!
## 5.2 Использование метода TThread.Synchronize для передачи данных в основной поток <a name="use_synchronize"></a>
В Delphi есть механизм, позволяющий дополнительному потоку выполнить запуск процедуры (метода) в контексте основного потока. Для этого предназначен метод `TThread.Synchronize`. Суть работы данного метода состоит в следующем:
1. Метод `Synchronize` запоминает в списке `SyncList` (глобальная переменная типа `TList`) переданный ему метод (метод представляет собой структуру-запись `TMethod`, в которой есть всего 2 поля: адрес функции и указатель на объект). Например, если мы выполним такой код: `TThread.Synchronize(nil, Form1.Show)`, то в списке будет сохранён указатель на объект (т.е. `Form1`) и адрес процедуры `TForm.Show`.
2. Основной поток периодически проверяет наличие элементов в списке `SyncList`. Если в списке обнаружен элемент, то производится запуск метода, хранящегося в списке.
3. Метод `TThread.Synchronize`, вызванный в дополнительном потоке, вернёт управление после того, как основной поток завершит вызов переданного ему метода.
:information_source: **Обратите внимание**, что в метод TThread.Synchronize можно передавать только методы-процедуры без параметров! Если нужны параметры, то Вы должны завести в своём классе потока дополнительные поля, присвоить им значения до вызова Syncronize, а затем использовать эти значения в методе, который вы передали в Syncronize.
:information_source: **Совет!** В современных версиях Delphi в метод Syncronize можно передавать анонимную процедуру. В этом случае намного реже возникает необходимость в объявлении дополнительных полей в классе потока.
:warning: **Внимание!** Каждый вызов TThread.Synchronize несёт в себе значительные накладные расходы, поэтому старайтесь использовать его как можно реже. В идеале, старайтесь обойтись без него.
:exclamation: **Внимание!** Нельзя уничтожать дополнительные потоки из метода, который Вы передаёте в TThread.Synchronize! Это приводит к зависанию программы.
Ниже представлен пример (Ex7), в котором дополнительный поток выполняет длительные вычисления (суммирует ряд чисел от 1 до `MaxValue`) и периодически вызывает метод `Synchronize` для отображения прогресса на главной форме.
```pascal
unit Ex7Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, MTUtils, ComCtrls;
type
TMyThread = class(TThread)
private
FResult: Int64;
FCurrValue: Integer;
procedure SetProgressParams;
procedure SetProgressCurrValue;
public
MaxValue: Integer;
procedure Execute; override;
end;
TForm1 = class(TForm)
btnRunInParallelThread: TButton;
ProgressBar1: TProgressBar;
Label1: TLabel;
labResult: TLabel;
edMaxValue: TEdit;
procedure btnRunInParallelThreadClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
FMyThread: TMyThread;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnRunInParallelThreadClick(Sender: TObject);
begin
// Уничтожаем запущенный поток
if Assigned(FMyThread) then
FreeAndNil(FMyThread);
// Создаём поток в спящем состоянии
FMyThread := TMyThread.Create(True);
// Запоминаем длину ряда в поле MaxValue
FMyThread.MaxValue := StrToIntDef(edMaxValue.Text, 0);
// Пробуждаем поток для выполнения вычислений
FMyThread.Resume;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FMyThread.Free;
end;
{ TMyThread }
procedure TMyThread.Execute;
var
Res: Int64;
CurrVal: Integer;
begin
// Выставляем параметры компонента ProgressBar1
Synchronize(SetProgressParams);
// Выполняем некоторые вычисления
Res := 0;
CurrVal := 0;
while CurrVal < MaxValue do
begin
if Terminated then Break;
Inc(CurrVal);
Res := Res + CurrVal;
if CurrVal mod 10000 = 0 then
begin // Обновление прогресса выполняется только 1 раз из 10000
FCurrValue := CurrVal;
FResult := Res;
Synchronize(SetProgressCurrValue);
end;
end;
// Обновляем прогресс в конце вычислений
FCurrValue := CurrVal;
FResult := Res;
Synchronize(SetProgressCurrValue);
end;
procedure TMyThread.SetProgressCurrValue;
begin
Form1.ProgressBar1.Position := FCurrValue;
Form1.labResult.Caption := IntToStr(FResult);
end;
procedure TMyThread.SetProgressParams;
begin
Form1.ProgressBar1.Max := MaxValue;
Form1.ProgressBar1.Position := 0;
Form1.labResult.Caption := '0';
end;
end.
```
Особенности данного примера:
1. Объект дополнительного потока создаётся с параметром `True` (`FMyThread := TMyThread.Create(True)`). Это означает, что метод `Execute` не будет вызван до тех пор, пока программа не вызовет метод `Resume` (либо `Start` в современных версиях Delphi). Таким образом, Вы можете создавать поток в замороженном состоянии, а запускать его позже, когда Вам будет удобно.
2. Запуск дополнительного потока осуществляется вызовом `FMyThread.Resume`.
:exclamation: **Внимание!** *В объявлении класса `TThread` рядом с методом `Resume` находится метод `Suspend`, предназначенный для принудительного перевода потока в спящее состояние. Вы не должны его использовать! Многие начинающие программисты пытаются управлять состоянием потока с помощью метода `Suspend`, однако, как правило, ни к чему хорошему это не приводит, кроме глюков в программе.*
3. Установка параметров компонента `ProgressBar1`, а также визуализация прогресса осуществляется вызовом метода `Synchronize`: `Synchronize(SetProgressParams)` и `Synchronize(SetProgressCurrValue)`.
:exclamation: **Внимание!** *Не забывайте вызывать метод `Synchronize`! Нет ничего проще, чем забыть это сделать (например, вызвать `SetProgressCurrValue` вместо `Synchronize(SetProgressCurrValue)`)! Delphi Вам не подскажет, зато при работе программы будут возникать сбои.*
4. Перед вызовом `Synchronize(SetProgressCurrValue)` выполняется установка значений полям `FCurrValue` и `FResult`, поскольку они в дальнейшем используются в методе `SetProgressCurrValue`.
5. Вызов `Synchronize(SetProgressCurrValue)` выполняется каждую десятетысячную итерацию цикла (см. условие `if CurrVal mod 10000 = 0 then…`). Если вызывать `Synchronize` чаще, то скорость вычислений замедлится, а нагрузка на главный поток программы возрастёт.
Обратите внимание, что для выхода из программы не обязательно дожидаться окончания работы потока. Вызов `FMyThread.Free` в методе `TForm1.FormDestroy` не приводит к зависанию программы, несмотря на то, что в методе `TMyThread.Execute` последней строкой осуществляется вызов метода `Synchronize`. Взаимной блокировки главного и дополнительного потока не происходит, т.к. в деструкторе объекта потока вызывается код, который предотвращает взаимную блокировку.
## 5.3 Периодическое чтение основным потоком данных, подготовленных в дополнительном потоке <a name="read_data_by_timer"></a>
В следующем примере (Ex8) задача визуализации текущего состояния вычислений, выполняющихся в дополнительном потоке, решается без использования метода `Synchronize`. На главной форме находится таймер, который через определённые промежутки времени (каждые 100 мс) считывает из объекта-потока `FMyThread` свойства `CalcResult`, `CurrValue` и `ThreadStateInfo` и отображает их значения на главной форме.
На мой взгляд, такой код становится проще и логичнее, чем при использовании метода `Synchronize`. Дополнительный поток занимается своим делом (выполняет вычисления) и не мешает главному потоку. Главный поток занимается своим делом – обслуживает интерфейс пользователя. Даже если главный поток подвиснет, что часто происходит при работе с базами данных, это никак не повлияет на работу дополнительного потока.
Вы не всегда сможете обойтись без `Synchronize`, но если задача позволяет и качество кода не ухудшится, то старайтесь сделать это. Но имейте ввиду, что сложные структуры, такие как строки, динамические массивы, объекты, варианты, и т.п. придётся (вероятно) защитить от одновременного доступа из разных потоков. Одной из целей данного примера является показать, как защитить строку `string`.
Ниже исходный код примера Ex8:
```pascal
unit Ex8Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, MTUtils, ComCtrls, ExtCtrls, SyncObjs;
type
TMyThread = class(TThread)
private
FMaxValue: Integer;
FResult: Int64;
FCurrValue: Integer;
// Информация о текущем состоянии потока
FThreadStateInfo: string;
function GetThreadStateInfo: string;
procedure SetThreadStateInfo(const Value: string);
public
constructor Create(MaxValue: Integer);
procedure Execute; override;
property CalcResult: Int64 read FResult;
property CurrValue: Integer read FCurrValue;
// Свойство для доступа к строке FThreadStateInfo с помощью
// потокозащищенных методов GetThreadStateInfo и SetThreadStateInfo
property ThreadStateInfo: string read GetThreadStateInfo
write SetThreadStateInfo;
end;
TForm1 = class(TForm)
btnRunInParallelThread: TButton;
ProgressBar1: TProgressBar;
Label1: TLabel;
labResult: TLabel;
edMaxValue: TEdit;
Timer1: TTimer;
Label2: TLabel;
labThreadStateInfo: TLabel;
procedure btnRunInParallelThreadClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
{ Private declarations }
FMyThread: TMyThread;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnRunInParallelThreadClick(Sender: TObject);
var
MaxValue: Integer;
begin
// Уничтожаем запущенный поток
if Assigned(FMyThread) then
FreeAndNil(FMyThread);
MaxValue := StrToInt(edMaxValue.Text);
ProgressBar1.Max := MaxValue;
ProgressBar1.Position := 0;
labResult.Caption := '0';
labThreadStateInfo.Caption := '???';
FMyThread := TMyThread.Create(MaxValue);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FreeAndNil(FMyThread);
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
if Assigned(FMyThread) then
begin
ProgressBar1.Position := FMyThread.CurrValue;
labResult.Caption := IntToStr(FMyThread.CalcResult);
labThreadStateInfo.Caption := FMyThread.ThreadStateInfo;
end;
end;
{ TMyThread }
constructor TMyThread.Create(MaxValue: Integer);
begin
FMaxValue := MaxValue;
inherited Create(False);
end;
procedure TMyThread.Execute;
begin
ThreadStateInfo := 'Start';
while FCurrValue < FMaxValue do
begin
if Terminated then Break;
Inc(FCurrValue);
FResult := FResult + FCurrValue;
ThreadStateInfo := Format('Progress: %f%%',
[FCurrValue / FMaxValue * 100]);
end;
ThreadStateInfo := 'Complete';
end;
function TMyThread.GetThreadStateInfo: string;
begin
// Защищаем строку с помощью критической секции. Если её убрать,
// то в главном потоке периодически будет возникать ошибка
// "Invalid pointer operation" либо "Out of memory"
StringProtectSection.Enter; // Входим в режим защиты
Result := FThreadStateInfo;
StringProtectSection.Leave; // Выходим из режима защиты
end;
procedure TMyThread.SetThreadStateInfo(const Value: string);
begin
StringProtectSection.Enter; // Входим в режим защиты
FThreadStateInfo := Value;
StringProtectSection.Leave; // Выходим из режима защиты
end;
end.
```
Особенности данного примера:
1. Введено дополнительное строковое поле `FThreadStateInfo` (информация о текущем состоянии потока). Доступ к этому полю осуществляется с помощью потокозащищённого свойства `ThreadStateInfo`, использующего методы `GetThreadStateInfo` и `SetThreadStateInfo` для чтения и записи строки `FThreadStateInfo`. Перед тем, как записать либо прочитать строку, выполняется вход в режим защиты с помощью кода `StringProtectSection.Enter`. После того, как работа со строкой завершилась, выполняется выход из режима защиты с помощью кода `StringProtectSection.Leave`.
:warning: **Внимание!** После каждого входа в режим защиты обязательно должен осуществляться выход из режима защиты.
:information_source: **Совет!** Защищать строки (и другие структуры и объекты) имеет смысл при наличии двух условий:
а) предполагается, что будет одновременный доступ к строке (структуре, объекту, массиву) из разных потоков;
б) строка (структура, объект, массив) не является «константной», т.е. она может меняться в зависимости от логики программы (в таких случаях используют ещё термин «mutable» или «мутабельный»).
:warning: **Внимание!** Если строка (структура, объект, массив) является «константной», т.е. если значение присваивается лишь один раз и больше не меняется, то нет смысла её защищать!
2. Глобальная переменная `StringProtectSection` (класс `TCriticalSection`) объявлена в модуле `MTUtils`, а соответствующий ей объект создаётся в секции `initialization`.
3. Непосредственный доступ к полям класса `TMyThread` (`FResult`, `FCurrValue` и `FThreadStateInfo`) допускается только из методов класса `TMyThread`. Сторонний код должен использовать соответствующие свойства: `CalcResult`, `CurrValue`, `ThreadStateInfo`. Свойства `CalcResult` и `CurrValue` обеспечивают доступ к полям `FResult` и `FCurrValue` только для чтения.
:information_source: *Что будет, если закомментировать строки `StringProtectSection.Enter` и `StringProtectSection.Leave`?* У меня в этом случае в программе иногда возникают следующие ошибки: "Invalid pointer operation" либо "Out of memory". Особенно легко ошибки воспроизвести, если запустить одновременно 4 экземпляра программы. Я не могу объяснить, из-за чего выдаётся "Out of memory", а с ошибкой "Invalid pointer operation" объяснение может быть таким: при каждом присвоении новой строки переменной `FThreadStateInfo` происходит две вещи:
а) выделяется новая область памяти под новую строку (с помощью функции менеджера памяти GetMem);
б) освобождается память, занятая старой строкой (с помощью функции менеджера памяти FreeMem).
Память будет освобождена при условии, если старая строка нигде больше не используется, т.е. у неё нулевой счётчик ссылок.
Главный поток при чтении незащищенной строки `FThreadStateInfo` может попасть в такую ситуацию, при которой:
а) память под старую строку уже освобождена с помощью FreeMem;
б) в переменной FThreadStateInfo всё ещё хранится указатель на старую строку. При попытке обращения к строке по недействительному указателю возникает ошибка «Invalid pointer operation».
Применение критической секции избавляет нас от этой проблемы.
## 5.4 Использование функции SendMessage для передачи данных в основной поток <a name="use_sendmessage"></a>
Мы уже рассмотрели возможность использования метода `Synchronize` для вызова указанного метода-процедуры в контексте основного потока. Также в Delphi существует альтернативный механизм передачи данных в основной поток, основанный на передаче и обработке оконных сообщений. Суть этого механизма состоит в том, что дополнительный поток вызывает стандартную Windows-функцию `SendMessage` (по ней я не планирую давать подробную информацию, поэтому читайте официальную документацию), а в классе формы (например, `TForm1`) реализуется метод обработки соответствующего оконного сообщения (этот метод вызывается в контексте основного потока). Вызов функции `SendMessage` является блокирующим, поэтому работа дополнительного потока будет приостановлена в точке вызова функции SendMessage до тех пор, пока не завершится работа метода обработки оконного сообщения.
Функция `SendMessage` объявлена следующим образом:
```pascal
function SendMessage(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
```
При вызове данной функции мы должны передать `Handle` компонента, в котором реализован обработчик сообщения (параметр `hWnd`), код сообщения (параметр `Msg`), а также, если Вам это необходимо, параметры (`wParam`, `lParam`).
Параметры `wParam` и `lParam` являются целочисленными (32 или 64 бита в зависимости от разрядности разрабатываемого приложения), поэтому с их помощью Вы можете передать а) целое число; б) ссылку на любой объект любого класса; в) указатель на любой блок выделенной памяти.
Метод обработки оконного сообщения объявляется с указанием ключевого слова message и кода сообщения. Пример объявления:
```pascal
type
TForm1 = class(TForm)
private
procedure UMProcessMyMessage(var Msg: TMessage); message WM_USER + 1;
public
end;
```
:warning: **Внимание!** *Если Вы вводите свои (нестандартные коды сообщений), то необходимо использовать коды не менее WM_USER, например:*
`WM_USER + 1, WM_USER + 2, WM_USER + 3` и т.д.
Рекомендуется заводить константы для нестандартных оконных сообщения (иначе Вы быстро забудете, что у Вас означает `WM_USER + 1`). Пример объявления такой константы:
```pascal
const
UM_PROCESS_MY_MESSAGE = WM_USER + 1;
```
Префикс «UM_» является сокращением от «User message».
Ниже представлен исходный код модуля из примера Ex9:
```pascal
unit Ex9Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, MTUtils, ComCtrls;
const
UM_PROGRESS_INIT = WM_USER + 1;
UM_PROGRESS_CHANGE = WM_USER + 2;
type
TProgressData = class
CurrValue: Integer;
CalcResult: Int64;
ThreadStateInfo: string;
end;
TMyThread = class(TThread)
private
ProgressData: TProgressData;
FFormHandle: THandle;
FMaxValue: Integer;
public
procedure Execute; override;
constructor Create(AMaxValue: Integer; AFormHandle: THandle);
destructor Destroy; override;
end;
TForm1 = class(TForm)
btnRunInParallelThread: TButton;
ProgressBar1: TProgressBar;
Label1: TLabel;
labResult: TLabel;
edMaxValue: TEdit;
Label2: TLabel;
labThreadStateInfo: TLabel;
procedure btnRunInParallelThreadClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
FMyThread: TMyThread;
procedure UMProgressInit(var Msg: TMessage); message UM_PROGRESS_INIT;
procedure UMProgressChange(var Msg:TMessage); message UM_PROGRESS_CHANGE;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnRunInParallelThreadClick(Sender: TObject);
begin
// Уничтожаем запущенный поток
if Assigned(FMyThread) then
FreeAndNil(FMyThread);
// Создаём и запускаем новый поток
FMyThread := TMyThread.Create(StrToInt(edMaxValue.Text), Handle);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FMyThread.Free;
end;
procedure TForm1.UMProgressChange(var Msg: TMessage);
var
ProgressData: TProgressData;
begin
ProgressData := TProgressData(Msg.WParam);
ProgressBar1.Position := ProgressData.CurrValue;
labResult.Caption := IntToStr(ProgressData.CalcResult);
labThreadStateInfo.Caption := ProgressData.ThreadStateInfo;
end;
procedure TForm1.UMProgressInit(var Msg: TMessage);
var
MaxValue: Integer;
begin
MaxValue := Msg.WParam;
ProgressBar1.Max := MaxValue;
ProgressBar1.Position := 0;
labResult.Caption := '0';
labThreadStateInfo.Caption := 'Start';
end;
{ TMyThread }
constructor TMyThread.Create(AMaxValue: Integer; AFormHandle: THandle);
begin
inherited Create(False);
FMaxValue := AMaxValue;
FFormHandle := AFormHandle;
ProgressData := TProgressData.Create;
end;
destructor TMyThread.Destroy;
begin
//ProgressData.Free; - НЕЛЬЗЯ ТУТ!
inherited;
ProgressData.Free;
end;
procedure TMyThread.Execute;
var
CurrVal: Integer;
begin
// Выставляем параметры компонента ProgressBar1
SendMessage(FFormHandle, UM_PROGRESS_INIT, FMaxValue, 0);
ThreadWaitTimeout(Self, 1000); // Просто пауза 1 сек.
CurrVal := 0;
// Выполняем некоторые вычисления
while CurrVal < FMaxValue do
begin
if Terminated then Break;
Inc(CurrVal);
ProgressData.CurrValue := CurrVal;
ProgressData.CalcResult := ProgressData.CalcResult + CurrVal;
ProgressData.ThreadStateInfo := Format('Progress: %f%%',
[CurrVal / FMaxValue * 100]);
// Обновление прогресса выполняется только 1 раз из 10000
if CurrVal mod 10000 = 0 then
SendMessage(FFormHandle, UM_PROGRESS_CHANGE, WPARAM(ProgressData), 0);
end;
// Обновляем прогресс в конце вычислений
SendMessage(FFormHandle, UM_PROGRESS_CHANGE, WPARAM(ProgressData), 0);
end;
end.
```
В данном примере используется два способа передачи данных в основной поток с использованием функции `SendMessage`. В первом способе (код `UM_PROGRESS_INIT`) целочисленное значение `FMaxValue` передаётся в качестве параметра `wParam`. Во втором способе (код `UM_PROGRESS_CHANGE`) передаётся ссылка на объект класса `TProgressData` в качестве параметра `wParam`. Для передачи ссылки на объект в качестве параметра `wParam` необходимо выполнять приведение типа к `WPARAM` следующим образом: `WPARAM(ProgressData)`. С `lParam` ситуация аналогична (тип `LPARAM`).
Объект `ProgressData` создаётся в конструкторе `TMyThread.Create`, а уничтожается в деструкторе `TMyThread.Destroy`. Для использования объекта в методе `TForm1.UMProgressChange` требуется использовать приведение типа к `TProgressData`: `ProgressData := TProgressData(Msg.WParam)`.
:exclamation: *Обратите внимание, что уничтожать объект `ProgressData` в деструкторе `TMyThread.Destroy` следует после того, как был вызван `inherited`.* Если уничтожать объект до вызова `inherited`, то при уничтожении объекта-потока (`FMyThread.Free`) будет возникать ошибка "Access violation" в том случае, если Вы попробуете закрыть программу, не дожидаясь окончания вычислений. Это объясняется следующим образом:
1) при вызове `FMyThread.Free` происходит вызов деструктора `TMyThread.Destroy`, в котором вызывается `inherited`;
2) при вызове `inherited` срабатывает родительский деструктор `TThread.Destroy`, в котором выставляется флаг `Terminated`;
3) внутри метода `TMyThread.Execute` всё ещё продолжается выполняться код и проверяется флаг `Terminated`. Объект `ProgressData` к этому моменту должен существовать, иначе при обращении к нему возникнет "Access Violation".
:information_source: *Обратите внимание, что при уничтожении объекта-потока (`FMyThread.Free`) главный поток не зависает, несмотря на то, что последней командой в методе `TMyThread.Execute` является `SendMessage`.* Это происходит благодаря тому, что в деструкторе потока есть код, который обеспечивает предотвращение взаимной блокировки.
:warning: **Внимание!** *Функцией `SendMessage` Вы можете пользоваться только при разработке программы под Windows.* Данная функция может быть недоступна при программировании под другие операционные системы! Универсальным (кроссплатформенным) способом передачи данных в основной поток является `TThread.Synchronize` либо `TThread.Queue` (в современных версиях Delphi).
## 5.5 Использование функции PostMessage для передачи очереди сообщений в основной поток <a name="use_postmessage"></a>
Функция `PostMessage` очень похожа на функцию `SendMessage` и также входит в состав Windows API. Однако существует принципиальная разница: функция `PostMessage` не является блокирующей, т.е. она вернёт управление сразу же после вызова. Из-за такой особенности Вы не всегда сможете использовать функцию `PostMessage` в тех же сценариях, что и `SendMessage` или `Synchronize`. При использовании функции `PostMessage` следует соблюдать осторожность, поскольку Вы можете столкнуться со следующими проблемами, которые не могут возникнуть при использовании `SendMessage`:
1) Переполнение оконной очереди сообщений из-за слишком частого вызова функции `PostMessage` (т.е. главный поток попросту не сможет обрабатывать сообщения из оконной очереди с той же скоростью, с которой сообщения добавляются в оконную очередь).
2) Задержка визуализации актуальной информации, поскольку обработка сообщений из оконной очереди выполняется по принципу FIFO. Допустим, дополнительный поток решил передать в основной поток 100 сообщений с помощью `PostMessage`, а главный поток при обработке каждого оконного сообщения выполняет запрос к базе данных, длительность которого составляет 1 секунду. Это приведёт к тому, что актуальная информация будет показана пользователю лишь спустя 100 секунд.
3) Обращение к недействительному указателю и, как следствие, ошибка «Invalid pointer operation» либо «Access violation». Такую ошибку легко получить, если дополнительный поток выполнит создание объекта, затем он вызовет функцию `PostMessage` (передаст в неё ссылку на объект), а затем сразу же уничтожит созданный ранее объект. Ошибка возникнет в основном потоке, как только в нём произойдет вызов обработчика оконного сообщения и он попытается обратиться к объекту по недействительному указателю.
Пример использования функции `PostMessage` для передачи данных в очередь сообщений основного потока находится в папке «Ex10». К сожалению, объём кода в примере получился большим, поэтому считаю, что нет необходимости приводить здесь весь пример целиком. Ниже приведён код метода `TMyThread.Execute`, а также код обработчика сообщения `UMProgressChange`:
```pascal
procedure TMyThread.Execute;
var
CurrVal: Integer;
CalcResult: Int64;
ThreadStateInfo: string;
ProgressData: TProgressData;
begin
CurrVal := 0;
CalcResult := 0;
// Выполняем некоторые вычисления
while CurrVal < FMaxValue do
begin
if Terminated then Break;
Inc(CurrVal);
CalcResult := CalcResult + CurrVal;
ThreadStateInfo := Format('Progress: %f%%', [CurrVal / FMaxValue * 100]);
// Обновление прогресса выполняется только 1 раз из 10000
if CurrVal mod 10000 = 0 then
begin
// Создаём объект ProgressData непосредственно перед PostMessage
ProgressData:=TProgressData.Create(CurrVal,CalcResult,ThreadStateInfo);
PostMessage(FFormHandle, UM_PROGRESS_CHANGE, WPARAM(ProgressData), 0);
Inc(PostMessageCount);
end;
end;
// Обновляем прогресс в конце вычислений
ProgressData := TProgressData.Create(CurrVal, CalcResult, ThreadStateInfo);
PostMessage(FFormHandle, UM_PROGRESS_CHANGE, WPARAM(ProgressData), 0);
Inc(PostMessageCount);
// Этот флаг необходим, чтобы главный поток мог убедиться, что
// доп. поток отработал корректно до последнего
EndWork := True;
end;
procedure TForm1.UMProgressChange(var Msg: TMessage);
var
ProgressData: TProgressData;
begin
ProgressData := TProgressData(Msg.WParam);
ProgressBar1.Position := ProgressData.CurrValue;
labResult.Caption := IntToStr(ProgressData.CalcResult);
labThreadStateInfo.Caption := ProgressData.ThreadStateInfo;
Inc(FPostMsgProcessCount);
// Здесь необходимо уничтожить объект TProgressData:
ProgressData.Free;
end;
```
Обратите внимание на то, что объект `ProgressData` создаётся в методе `TMyThread.Execute`, а уничтожается в методе `UMProgressChange`.
В окне программы отображается количество вызовов функции `PostMessage` и количество вызовов обработчика `UMProgressChange`. В данном примере вызов функции `PostMessage` выполняется 1 раз из 10000.
:warning: *Если производить вызов 1 раз из 100 (или чаще), то будет происходить переполнение оконной очереди сообщений и обработчик `UMProgressChange` будет срабатывать меньшее количество раз по сравнению с количеством вызовов функции `PostMessage`.* Для нашего примера это плохо тем, что будет происходить утечка памяти, поскольку перед каждым вызовом функции `PostMessage` выполняется создание объекта `ProgressData`. Если обработчик `UMProgressChange` будет срабатывать реже, чем `PostMessage`, то не все объекты `ProgressData` будут уничтожены.
## 5.6 Использование списка TThreadList для передачи очереди данных в основной поток <a name="use_thread_list"></a>
При разработке многопоточных программ очень часто возникает необходимость организовать передачу очереди данных между потоками. Для организации такой очереди можно использовать массивы или списки. Очень важно, чтобы массив / список был защищён от одновременного доступа из нескольких потоков. В простейшем случае мы можем защитить массив / список с помощью критической секции. В Delphi имеются специализированные классы, позволяющие организовать очередь сообщений для обмена данными между потоками. Одним из таких классов является `TThreadList` – потокобезопасный список. По сути, это обычный `TList`, в котором используется критическая секция. Он не допускает одновременного доступа к списку из разных потоков.
В примере Ex11 будет решена такая задача: несколько дополнительных потоков будут генерировать текстовую информацию (события), а главный поток будет отображать дату, время, номер потока и текст события в компоненте `TListBox`.
Ниже представлен листинг кода примера Ex11:
```pascal
unit Ex11Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, MTUtils, ComCtrls, ExtCtrls, StrUtils, Contnrs;
type
TLogMessage = class
ThreadId: Integer;
EventTime: TDateTime;
EventText: string;
constructor Create(AThreadId: Integer; AEventTime: TDateTime;
AEventText: string);
end;
TMyThread = class(TThread)
private
procedure LogEvent(s: string);
protected
procedure Execute; override;
end;
TForm1 = class(TForm)
btnRunInParallelThread: TButton;
Timer1: TTimer;
Button1: TButton;
ListBox1: TListBox;
procedure btnRunInParallelThreadClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
FList: TObjectList;
public
{ Public declarations }
end;
var
Form1: TForm1;
ThreadCounter: Integer;
EventList: TThreadList;
implementation
{$R *.dfm}
procedure TForm1.btnRunInParallelThreadClick(Sender: TObject);
begin
// Создаём и запускаем новый поток
FList.Add(TMyThread.Create(False));
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
FList.Clear;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FList := TObjectList.Create;
EventList := TThreadList.Create;;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FList.Free;
// В списке могут остаться элементы. Это не страшно, поскольку
// выполняется выход из программы
EventList.Free;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
L, TmpList: TList;
I: Integer;
m: TLogMessage;
Cnt: Integer;
begin
// Определяем, есть ли элементы в списке EventList
L := EventList.LockList;
Cnt := L.Count;
EventList.UnlockList;
if Cnt = 0 then Exit;
TmpList := TList.Create;
try
L := EventList.LockList;
try
// Переносим все элементы во временный список
TmpList.Assign(L);
// Очищаем список EventList
L.Clear;
finally
// Как можно быстрее снимаем блокировку списка
EventList.UnlockList;
end;
// Дальше обрабатываем элементы из временного списка
for I := 0 to TmpList.Count - 1 do
begin
m := TLogMessage(TmpList[I]);
if ListBox1.Count < 50000 then
ListBox1.Items.Add(Format('%s [T:%d] - %s',
[FormatDateTime('hh:nn:ss.zzz', m.EventTime), m.ThreadId, m.EventText]));
// Здесь необходимо уничтожить объект TLogMessage
m.Free;
end;
ListBox1.ItemIndex := ListBox1.Count - 1;
finally
TmpList.Free;
end;
end;
{ TMyThread }
procedure TMyThread.Execute;
var
I: Integer;
begin
I := 0;
LogEvent('Thread start');
while not Terminated do
begin
Inc(I);
LogEvent('Event #' + IntToStr(I));
ThreadWaitTimeout(Self, 1000);
end;
LogEvent('Thread stop');
end;
procedure TMyThread.LogEvent(s: string);
var
L: TList;
m: TLogMessage;
begin
m := TLogMessage.Create(GetCurrentThreadId, Now, s);
L := EventList.LockList;
L.Add(m);
EventList.UnlockList;
end;
{ TLogMessage }
constructor TLogMessage.Create(AThreadId: Integer; AEventTime: TDateTime;
AEventText: string);
begin
ThreadId := AThreadId;
EventTime := AEventTime;
EventText := AEventText;
end;
end.
```
В данном примере используется таймер TTimer, который срабатывает каждые 100 мс. В обработчике таймера `TForm1.Timer1Timer` выполняются следующие действия:
1) Проверяется количество элементов в списке `EventList`;
2) Если в списке `EventList` есть элементы, то они копируются во временный список `TmpList`, а из списка `EventList` все элементы удаляются;
3) Элементы из списка `TmpList` выводятся в список `ListBox1`.
Обратите внимание, как следует пользоваться объектом класса `TThreadList`. Для начала необходимо выполнить блокировку списка с помощью метода `LockList` и запомнить ссылку на объект класса `TList`: `L := EventList.LockList`. Далее с полученной ссылкой вы можете производить любые действия (добавление, удаление, очистка элементов списка `TList`). После того, как Вы закончили работать со списком, необходимо его разблокировать. Для этого следует вызвать метод `UnlockList`.
:information_source: **Внимание!** *Не следует устанавливать блокировку на длительное время!* Старайтесь проектировать свой код таким образом, чтобы длительность блокировки была как можно меньше.
Обратите внимание, что в обработчике `TForm1.Timer1Timer` вызов методов `LockList` / `UnlockList` выполняется дважды. Сначала мы блокируем список для доступа к количеству элементов (свойство `TList.Count`). При этом мы не используем конструкцию `try..finally`. Далее мы блокируем список `EventList` для копирования элементов во временный список `TmpList` и в этом случае мы используем конструкцию `try..finally`.
:information_source: **Внимание!** *Решение о том, нужно ли использовать конструкцию `try..finally` в каждом конкретном случае, зависит от того, какова вероятность возникновения ошибки (exception) между LockList и UnlockList.* Если код очень простой и ошибка возникнуть не может (например, при обращении к свойству `Count`), то использовать конструкцию `try..finally` нет необходимости. В противном случае рекомендуется подстраховаться и использовать конструкцию `try..finally`, чтобы гарантировать, что метод `UnlockList` будет вызван при любой ситуации. Также рекомендуется использовать `try..finally` для улучшения визуального восприятия структуры кода в случае значительного количества строк кода между `LockList` и `UnlockList`. В некоторых задачах (например, при математических вычислениях) следует по возможности избегать использования конструкций `try..finally` и `try..except`, т.к. они могут нести значительные накладные расходы.
В данном примере используется функция `GetCurrentThreadId` из состава Windows API. Данная функция возвращает идентификатор того потока, который вызвал эту функцию. Идентификатор каждого потока в операционной системе является уникальным, вне зависимости от того, какой процесс его создал.
## 5.7 Использование метода TThread.Synchronize для передачи данных в основной поток – вариант с использованием анонимной процедуры <a name="use_synchronize_anonimous"></a>
В современных версиях Delphi можно передавать анонимную процедуру при вызове метода `TThread.Synchronize`.
Анонимная процедура / функция, как следует из названия, не имеет имени и может использоваться в качестве параметра при вызове другой процедуры/функции. Механизм работы анонимных функций довольно сложный и у меня нет возможности его здесь описывать (в этом нет ничего страшного, поскольку в интернете есть много информации на данную тему). Хочу отметить, что анонимные функции давно являются нормой в современных языках программирования. Термин «анонимная функция» используется в Delphi (на мой взгляд, это не очень удачное название, поскольку очень часто анонимная функция присваивается переменной, а переменная имеет вполне конкретное имя). В других языках программирования данный механизм может называться иначе, например: делегат, замыкание, лямбда. В независимости от того, как этот механизм называется в том или ином языке, существует одна очень важная особенность: анонимная функция «видит» все переменные, которые являются внешними по отношению к тому месту, в котором она расположена. В случае передачи анонимной процедуры в метод `Synchronize`, её вызов будет произведён в контексте главного потока, однако внутри анонимной процедуры мы вполне можем использовать локальные переменные, которые были объявлены в методе `Execute`.
Ниже представлен простейший пример вызова метода `Synchronize` с анонимной процедурой из метода `Execute`:
```pascal
CurTime := Now; // Запоминаем время ДО вызова Synchronize
Synchronize(
procedure
begin
Form1.labLabLastThreadTime.Caption :=
'Последний поток был запущен: ' + DateTimeToStr(CurTime);
end);
```
## 5.8 Использование метода TThread.Queue для передачи данных в основной поток <a name="use_tthread_queue"></a>
В современных версиях Delphi имеется ещё один метод, позволяющий произвести вызов заданной процедуры в контексте основного потока: `TThread.Queue`. Данный метод имеет параметры, аналогичные методу `TThread.Synchronize`. Основное отличие от метода `Synchronize` заключается в том, что метод `Synchronize` является блокирующим, а метод `Queue` блокирующим не является. Также как и функция `PostMessage`, метод `Queue` возвращает управление немедленно. Метод `Queue`, как и метод Synchronize, может принимать в качестве параметра:
а) метод (процедуру);
б) анонимную процедуру;
в) обычную процедуру (в этом случае компилятор создаёт анонимную процедуру, из которой производится вызов обычной процедуры).
Суть работы метода состоит TThread.Queue в следующем:
1) Метод `Queue` добавляет в конец списка `SyncList` (глобальная переменная типа `TList`) информацию, необходимую для вызова переданной процедуры.
2) Основной поток периодически проверяет наличие элементов в списке `SyncList`. Если в списке обнаружен элемент, то производится запуск процедуры, хранящейся в списке.
3) Метод `TThread.Queue`, вызванный в дополнительном потоке, вернёт управление немедленно, т.е. он не будет дожидаться, когда основной поток выполнит запуск заданной процедуры.
С появлением метода `TThread.Queue` многие Delphi-программисты стали гораздо реже использовать старые способы взаимодействия с основным потоком (`Synchronize`, `SendMessage`, `PostMessage`). Вероятная причина этого заключается в том, что дополнительный поток не блокируется при вызове `Queue`, и никак не зависит от того, насколько сильно в данный момент загружен главный поток программы. Метод `TThread.Queue` можно использовать в большинстве тех случаев, когда раньше приходилось использовать метод `Synchronize`. Если Вы используете метод `Queue`, то должны учитывать, что при вызове заданной процедуры в контексте основного потока дополнительный поток продолжает выполняться и существует риск одновременного обращения к объекту / структуре / массиву / строке из главного и из дополнительного потока. Мы помним, что в некоторых случаях мы должны защищать сложные объекты от одновременного доступа, например, с помощью критической секции, либо с помощью механизма `TMonitor`.
Пример Ex12 демонстрирует применение метода `TThread.Queue` для журналлирования событий, происходящих в дополнительном потоке.
```pascal
unit Ex12Unit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, MTUtils, ComCtrls, ExtCtrls, StrUtils, Contnrs,
Generics.Collections;
type
TMyThread = class(TThread)
private
procedure LogEvent(EventText: string);
protected
procedure Execute; override;
end;
TForm1 = class(TForm)
btnRunInParallelThread: TButton;
Button1: TButton;
ListBox1: TListBox;
labLabLastThreadTime: TLabel;
btnClearListBox: TButton;
procedure btnRunInParallelThreadClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure btnClearListBoxClick(Sender: TObject);
private
{ Private declarations }
FList: TObjectList<TMyThread>;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnClearListBoxClick(Sender: TObject);
begin
ListBox1.Clear;
end;
procedure TForm1.btnRunInParallelThreadClick(Sender: TObject);
begin
// Создаём и запускаем новый поток
FList.Add(TMyThread.Create(False));
end;
procedure TForm1.Button1Click(Sender: TObject);
var
t: TMyThread;
begin
for t in FList do
t.Terminate; // Сообщаем потокам о необходимости завершаться
FList.Clear; // Уничтожаем потоки
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FList := TObjectList<TMyThread>.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FList.Free;
end;
{ TMyThread }
procedure TMyThread.Execute;
var
I: Integer;
CurTime: TDateTime;
begin
CurTime := Now; // Запоминаем время ДО вызова Queue
Queue(
procedure
begin
Form1.labLabLastThreadTime.Caption :=
'Последний поток был запущен: ' + DateTimeToStr(CurTime);
end);
LogEvent('Thread start');
I := 0;
while not Terminated do
begin
Inc(I);
LogEvent('Event #' + IntToStr(I));
ThreadWaitTimeout(Self, 500);
end;
LogEvent('Thread stop');
end;
procedure TMyThread.LogEvent(EventText: string);
var
ThreadId: Cardinal;
EventTime: TDateTime;
begin
// Запоминаем ID потока и текущее время ДО вызова Queue
ThreadId := GetCurrentThreadId;
EventTime := Now;
TThread.Queue(nil,
procedure
begin
Form1.ListBox1.Items.Add(Format('%s [T:%d] - %s',
[FormatDateTime('hh:nn:ss.zzz', EventTime), ThreadId, EventText]));
Form1.ListBox1.ItemIndex := Form1.ListBox1.Count - 1;
end);
end;
end.
```
Обратите внимание на следующие особенности данного примера:
1) В методе `TMyThread.LogEvent` при вызове метода `Queue` используется следующая форма: `TThread.Queue(nil, АнонимнаяПроцедура)`. Благодаря тому, что используется значение `nil`, анонимная процедура не ассоциируется с объектом дополнительного потока. Благодаря этому гарантируется, что основной поток обработает все вызовы `Queue` с параметром `nil`. Даже если дополнительный поток 1000 раз подряд вызовет метод `Queue`, а главный поток вызовет метод `Free` для объекта-потока, не успев обработать все вызовы `Queue`, объект потока будет уничтожен, но главный поток всё равно обработает все вызовы.
:exclamation: *Следует быть предельно внимательным при использовании такой формы вызова метода Queue.* Дело в том, что если в анонимной процедуре будут обращения к полям / свойствам / методам объекта-потока, то в ситуации досрочного уничтожения объекта потока произойдёт ошибка доступа к памяти (например, «Invalid pointer operation», «Access violation» либо «Out of memory») при вызове процедуры в контексте основного потока.
2) В анонимной процедуре, которая является параметром метода `Queue` в методе `TMyThread.LogEvent`, отсутствуют обращения к полям / свойствам / методам объекта-потока. Реализация данной анонимной процедуры никак не привязана к состоянию дополнительного потока. В ней не произойдет ошибки, даже если объект-поток будет уничтожен раньше времени.
3) В методе `TMyThread.LogEvent` все данные, которые используются в анонимной процедуре, формируются заранее. В анонимной процедуре используются переменные `EventTime`, `ThreadId`, `EventText`. Переменные `EventTime` и `ThreadId` объявляются в методе `TMyThread.LogEvent` и им присваиваются значения до вызова `Queue`. Переменная `EventText` является параметром метода `LogEvent`, т.е. её значение также сформировано до вызова `Queue`.
:information_source: *К сожалению, в метод `Queue` невозможно передать дополнительные параметры, которые в дальнейшем могли бы использоваться при вызове анонимной процедуры.* Но благодаря тому, что переменные `EventTime`, `ThreadId`, `EventText` объявлены в методе `LogEvent` и их значения сформированы до вызова `Queue`, то мы можем их рассматривать как параметры вызова анонимной процедуры. При каждом вызове метода `Queue` (внутри метода `LogEvent`) переменные `EventTime`, `ThreadId`, `EventText` будут «захватываться», а затем использоваться при вызове анонимной процедуры в основном потоке.
4) Метод `Queue` вызывается из отдельного метода `TMyThread.LogEvent`. **Это очень важно!** Если мы хотим эмулировать передачу параметров в анонимную функцию, то вызов метода `Queue` лучше выполнять из отдельного метода. Например:
```pascal
procedure TMyThread.CallMyFuncInMainThread(param1: Integer; param2: string);
begin
TThread.Queue(nil,
procedure
var
s: string;
begin
s := Format('param1=%d; param2=%s', [param1, param2]);
Form1.ListBox1.Items.Add(s);
end);
end;
```
Вызов из Execute:
```pascal
I := 111;
sVal := 'Text 1';
CallMyFuncInMainThread(I, sVal);
I := 222;
sVal := 'Text 2';
CallMyFuncInMainThread(I, sVal);
```
Благодаря этому, при передаче анонимной процедуры в метод `Queue` происходит захват параметров и переменных отдельного метода. Если бы мы в методе `Execute` выполнили 2 раза подряд вызов `Queue` вместо `CallMyFuncInMainThread`, например:
```pascal
I := 111;
sVal := 'Text 1';
TThread.Queue(nil,
procedure
var
s: string;
begin
s := Format('param1=%d; param2=%s', [I, sVal]);
Form1.ListBox1.Items.Add(s);
end);
I := 222;
sVal := 'Text 2';
TThread.Queue(nil,
procedure
var
s: string;
begin
s := Format('param1=%d; param2=%s', [I, sVal]);
Form1.ListBox1.Items.Add(s);
end);
```
то для обоих вызовов анонимной процедуры использовались бы одни и те же значения захваченных переменных `I` и `sVal`. В этом случае в `ListBox1` будет добавлено 2 одинаковых строки:
`param1=222; param2=Text 2`
`param1=222; param2=Text 2`
Конечно, если после первого вызова `Queue` поставить паузу (например, `Sleep(100)`), то главный поток скорее всего успеет выполнить вызов первой анонимной процедуры с правильными значениями 111 и «Text 1».
5) В методе `Execute` присутствует вызов метода `Queue`, в который передаётся анонимная процедура, которая записывает в компонент `labLabLastThreadTime` время запуска дополнительного потока. Обратите внимание, что здесь используется форма вызова без указания `nil`. Это означает, что анонимная процедура будет ассоциирована с объектом потока, что в свою очередь означает, что при уничтожении потока необработанный главным потоком вызов `Queue` (вернее, информация, необходимая для вызова процедуры), будет удалена из очереди отложенных вызовов. В данном случае нет никаких рисков по следующим причинам:
а) метод `Queue` вызывается в начале метода `Execute`, поток не завершится сразу же после данного вызова `Queue` (пользователь просто не успеет так быстро нажать кнопку «Остановить потоки»), значит главный поток успеет обработать данный вызов `Queue`.
б) даже если главный поток не успеет выполнить отложенный вызов процедуры, а дополнительный поток по каким-то причинам будет уничтожен, необходимая для вызова отложенной процедуры информация попросту будет удалена из очереди. В данном случае это не приведёт к негативным последствиям, поскольку процедура не выполняет никакой ответственной работы.
:information_source: **Внимание!** *Будьте внимательны при использовании формы вызова метода Queue без указания nil!* В этом случае вызов отложенной процедуры может не произойти (если дополнительный поток был уничтожен до того, как главный поток произвёл вызов процедуры). В некоторый случаях это может приводить к некорректной работе программы (если программа написана таким образом, что вызов отложенной процедуры является обязательным), либо к утечке памяти (если отложенная процедура должна освободить память, выделенную перед вызовом метода `Queue`).
6) В методе `Execute` организована пауза 500 мс между вызовами метода `LogEvent` в цикле.
:warning: **Внимание!** *Не следует вызывать метод Queue слишком часто.* Если дополнительный поток будет вызывать метод `Queue` слишком часто, то главный поток программы не будет успевать обрабатывать элементы очереди `SyncList`, в результате чего размер списка отложенных вызовов `SyncList` будет увеличиваться, а интерфейс пользователя будет выглядеть зависшим.
7) В данном примере для хранения ссылок на объекты-потоки используется список, объявленный следующим образом: `FList: TObjectList<TMyThread>`. Это позволяет избавиться от необходимости использовать операцию приведения типа (как при использовании обычного `TObjectList`).
Для исследования работы метода `Queue` вы можете использовать пример `Ex12Full`. Он наглядно демонстрирует проблему обработки отложенных вызовов при уничтожении дополнительных потоков. Его особенности следующие:
1) Понижается приоритет дополнительного потока с помощью кода `Priority := tpLowest`. Это означает, что потоку будет отводиться гораздо меньше процессорного времени, чем обычно. Цель этого - сделать так, чтобы код, который выполняется в конце метода `Execute` (цикл от 1 до 100) занимал процессор на минимальное количество времени. По этим же причинам в данном цикле дополнительно вызывается `Sleep(0)`. Благодаря этому главный поток начнёт обработку очереди отложенных вызовов раньше, чем завершится цикл, т.е. что-нибудь из этого цикла скорее всего попадёт в `ListBox`.
2) Переменной `ThreadWaitTimeoutSleepTime`, которая используется в процедуре `ThreadWaitTimeout`, выставляется значение «1». Это означает, что процедура `ThreadWaitTimeout` завершит свою работу сразу же, как только объекту потока будет произведён вызов метода `Terminate`. Это важно в том случае, если Вы запустили несколько потоков. Благодаря этому у всех потоков цикл от 1 до 100 будет выполняться практически одновременно.
3) В анонимной процедуре, которая реализована в методе `TMyThread.LogEvent`, вызывается `Sleep(5)`. Это необходимо для того, чтобы главный поток не мог сразу (за свой квант времени) обработать все сообщения из очереди.
4) Реализовано 2 способа вызова метода `TThread.Queue` в методе `TMyThread.LogEvent`: 1) ассоциировать анонимную процедуру с объектом потока; 2) не ассоциировать. Если выбран способ 1, то при уничтожении потоков в список `ListBox` попадут не все строки от 1 до 100. Это означает, что при уничтожении объекта-потока произошло удаление всех отложенных вызовов (т.е. записей из списка `SyncList`), которые ассоциированы с объектом потока, но ещё не обработаны главным потоком. Если выбран способ 2, то в `ListBox` попадут все строки от 1 до 100.
# 6. Планирование потоков <a name="os_scheduler"></a>
В разделе 1.3 было коротко сказано о том, что в операционной системе имеется планировщик (scheduler), который отвечает за распределение процессорного времени между потоками. Иногда его называют "диспетчер ядра" (kernel’s dispatcher). Я считаю, что читатель должен иметь хотя бы минимальное представление о работе планировщика. Благодаря этому усвоение такого сложного материала, как синхронизация между потоками, должно пройти легче.
Для получения дополнительной информации о работе механизма планирования потоков в Windows рекомендую изучить [документацию на сайте microsoft.com](https://docs.microsoft.com/en-us/windows/win32/procthread/scheduling).
Если Вы хотите вникнуть в детали реализации планировщика, то рекомендую статью Марка Руссиновича и Дэвида Соломона [Processes, Threads, and Jobs in the Windows Operating System](https://www.microsoftpressstore.com/articles/article.aspx?p=2233328&seqNum=7).
# 6.1 Роль системного таймера (system clock) <a name="os_timer"></a>
Как уже было сказано в разделе 1.3, системный планировщик автоматически выполняет действия, необходимые для перевода потока в спящий режим, как только поток истратит выделенный ему квант времен (time slice), либо процессорное время потребуется потоку с более высоким приоритетом. Также поток может самостоятельно перейти в спящий режим в тех случаях, если он выполнил свою работу, либо перешёл в режим ожидания события. Каждый раз, когда поток переходит в спящий режим, происходит сохранение его контекста (сохраняется текущее состояние регистров процессорного ядра, на котором поток выполняется). При возобновлении работы потока выполняется обратное действие – восстановление контекста потока (загрузка ранее сохраненных значений в регистры процессорного ядра).
Важно уточнить, что поток не может самостоятельно выйти из спящего режима. Возобновление работы потока происходит в следующих случаях:
а) в результате обработки сигнала прерывания от системного таймера;
б) в результате вызова одним из потоков функции ядра ОС (например, `Sleep`, `SwitchToThread`, `SetEvent` и др.)
:information_source: **Информация** В ОС Windows адресное пространство системы отделено от адресного пространства пользовательских программ. Если пользовательская программа вызывает функцию ядра (например, `Sleep`, `SwitchToThread`, `SetEvent` и др.), то ОС выполняет переключение процессора в режим ядра (kernel mode), после которого дальнейший код этих функций выполняется в контексте адресного пространства ядра операционной системы). При завершении работы функций ядра производится обратное переключение процессора в пользовательский режим и восстановление состояния регистров процессора.
Процесс планирования работы потоков выполняется системным планировщиком периодически при каждом срабатывании системного таймера (system clock). Раньше системный таймер представлял собой отдельную микросхему, которая выдавала на процессор аппаратный сигнал прерывания приблизительно раз в 16 миллисекунд. Что представляет собой современный системный таймер, я не знаю. Вероятно, он встроен в процессор или эмулируется программно.
Логика обработки прерывания от системного таймера примерно такая:
1) системный таймер подаёт аппаратный сигнал прерывания на процессор;
2) осуществляется запуск системного планировщика на одном из ядер процессора;
3) планировщик анализирует состояние всех потоков в системе и принимает решение о необходимости запуска или приостановки того или иного потока (он сохраняет необходимую информацию в память ядра ОС);
4) планировщик подаёт на процессор сигнал программного прерывания APC (асинхронный вызов процедуры) в том случае, если необходимо приостановить либо возобновить работу потока на том или ином ядре процессора (если же требуется остановить либо запустить поток на том же ядре, на котором выполняется планировщик, то есть возможность обойтись без сигнала прерывания APC);
5) выполняется обработка сигнала прерывания APC (на том или ином ядре процессора), в результате которой одни потоки приостанавливаются, а другие возобновляют свою работу;
Частота срабатывания (разрешение) системного таймера составляет от 1 мс до 16 мс. Причем, по умолчанию используется значение 16 мс. Но это значение очень часто переопределяется программным способом (некоторые программы его меняют на 1 мс). На практике приходится видеть только одно из двух значений: 1 мс либо 16 мс. Для того, чтобы определить разрешение системного таймера, Вы можете воспользоваться утилитой Марка Руссиновича "Clockres.exe". Значение "Current timer interval" показывает разрешение системного таймера.
Почему важно знать разрешение системного таймера? Очень просто: чем выше разрешение системного таймера (самое высокое при интервале 1 мс), тем чаще срабатывает прерывание от системного таймера и планировщик задач чаще выполняет мониторинг потоков. Но при этом сам планировщик задач создаёт более высокую нагрузку на процессор (аккумулятор в ноутбуке при этом будет быстрее разряжаться). Разумеется, чем меньше разрешение системного таймера (самое низкое при интервале 16 мс), тем реже срабатывает планировщик и нагрузка на процессор будет меньше.
:warning: **Внимание!** Не следует увеличивать частоту срабатывания системного таймера! Программы от этого работать быстрее не будут! Наоборот, общая производительность снизится (по имеющимся оценкам, более чем на 2%).
:information_source: **Частота срабатывания системного таймера сильно влияет на работу функции `Sleep`!** Если таймер срабатывает очень часто (1000 раз в секунду), то функция Sleep работает с максимальной точностью (плюс/минус 0.5 мс). Если таймер срабатывает раз в 16 мс, то и точность работы функции Sleep составит примерно 16 мс.
:information_source: Разрешение системного таймера **никак не влияет** на функции `GetTickCount` и `GetTickCount64`. Их точность соответствует максимальному интервалу системного таймера, т.е. примерно 16 мс. В связи с этим я не советую использовать эти функции для измерения интервалов времени (хотя вроде для этого они и были предназначены). Если Вас устраивает точность замеров 1 мс, то лучше используйте обычную функцию `Now`. Если требуется производить замеры в большей точностью, то используйте функцию `QueryPerformanceCounter`, либо более удобный модуль "TimeIntervals.pas", который ранее уже был рассмотрен.
Отметим также следующие особенности планирования потоков в Windows:
1. Для каждого ядра процессора планировщик создаёт свою очередь потоков, готовых к запуску. При обращениях к планировщику, не связанных с работой системного таймера (например, при вызове функций ядра `Sleep`, `SwitchToThread`, `SetEvent`), анализ всех потоков не выполняется. Учитывается только информация, заранее подготовленная планировщиком, поэтому такой код диспетчера ядра выполняется максимально быстро.
2. При вызове функции `SwitchToThread` выполняется анализ очереди потоков, готовых к запуску, на том же ядре, на котором был запущен поток, который вызвал функцию `SwitchToThread`. Переключение на другой поток произойдёт максимально быстро, за несколько микросекунд (т.к. не требуется использовать прерывание APC), либо переключения на другой поток не произойдёт.
3. Функция `Sleep(0)` отличается от `SwitchToThread` тем, что учитываются потоки, готовые к запуску, не только на этом, но и на других ядрах процессора. Если на ядре CPU1 поток вызвал функцию `Sleep(0)`, а на ядре CPU2 имеется готовый к запуску поток, то его запуск может быть отменён на ядре CPU2 и произведён на ядре CPU1 (скорее всего без использования прерывания APC, поэтому переключение на другой поток займёт всего несколько микросекунд). Если готового к запуску потока нет ни на одном ядре процессора, то произойдет выход из функции `Sleep(0)`.
:warning: *Я привёл здесь очень упрощенный алгоритм работы планировщика. Это лишь вершина айсберга. На самом деле эффективное планирование потоков - это очень сложная задача, при решении которой необходимо учитывать тысячи нюансов. Я думаю, что общий объём кода диспетчера ядра Windows может достигать сотен тысяч строк программного кода.*
# 6.2 Влияние количества запущенных потоков на производительность системы <a name="thread_count_cost"></a>
Очевидно, что чем больше потоков, тем хуже производительность системы. Я предполагаю, что при каждом запуске системного планировщика по прерыванию от системного таймера он производит анализ всех запущенных потоков, независимо от их состояния. Чем больше потоков, тем больше действий будет выполнять планировщик.
Однако большое количества потоков, находящихся в спящем состоянии, не оказывает никакого влияния на процесс переключения контекста между активными потоками. Т.е. если поток вызвал функцию `Sleep(0)` или `SwitchToThread`, то учитываются данные, заранее подготовленые планировщиком, лишние действия не выполняются.
<!--# 6.2 Стоимость переключения контекста потоков <a name="context_cost"></a>
Переключение контекста потока имеет весьма большую стоимость (несколько тысяч тактов ядра процессора). Она мало зависит от того, на каких ядрах процессора работает поток в тот или иной момент времени. Операции сохранения и восстановления состояния регистров при переключении контекста практически не играют никакой роли. Думаю, что и операция переключения между режимами процессора "user-mode" и "kernel-mode" большой роли не играют. Скорее всего наибольшее время занимает код, исполняемый операционной системной в режиме "kernel-mode", который анализирует указания, полученные от системного планировщика состояние остальных потоков в системе, состояние объектов синхронизации ядра (в том числе мьютексов, эвентов, семафоров). Чем больше в системе запущенных потоков (работающих либо спящих) и чем больше объектов синхронизации ядра, тем больше времени будет занимать операция переключения контекста. А если к этому добавить ещё и срабатывание системного таймера 1000 раз в секунду, то код планировщика и код переключения контекста могут занять большую часть ресурсов процессора.
-->
<!--# 6.2 Роль кэшей процессора <a name="cpu_cache"></a>
Важно знать, что ядро процессора не может работать с памятью ОЗУ напрямую (будем считать, что ОЗУ - это память, которая представлена платами ОЗУ, например DDR4). Ему доступны непосредственно только регистры и кэш первого уровня ядра L1 (иногда его называют "память ядра").
Если процессор обращается к ячейке памяти, а её значения нет в кэше первого уровня ядра, то кэш первого уровня запросит эти данные у кэша второго уровня ядра L2. В свою очередь, если у кэша второго уровня ядра нет этих данных, то выполняется обращение к кэшу третьего уровня процессора L3. Если и у кэша третьего уровня этих данных нет, то он берёт их из памяти ОЗУ. Если же у кэша третьего уровня эти данные имеются, но он не уверен в их актуальности, то выполняется обращение к кэшам других ядер процессора.
На самом деле, алгоритмы работы кэшей и механизм обеспечения согласованности (когерентности) данных между процессорами и их ядрами являются внутренней кухней процессора. Каждый производитель решает эти задачи по своему.
Что из этого важно знать нам:
1. Самый быстрый вид памяти - регистровая. Скорость работы регистров определяется текущей частотой, на которой работает то или иное ядро процессора.
2. Кэши процессора являются частью оперативной памяти. Т.е. оперативная память состоит из памяти ОЗУ и памяти кэшей процессора.
3. Доступ к памяти кэшей процессора происходит значительно быстрее, чем к памяти ОЗУ (во многом, благодаря меньшему расстоянию от ядра до памяти кэша).
4. Кэш первого уровня ядра - это самая быстрая часть оперативной памяти.
5. Если код, который выполняется на ядре процессора, выполнил запись в оперативную память, это означает, что он выполнил запись в кэш первого уровня ядра. Вовсе не обязательно, что новое значение будет передано в ОЗУ. Решение об этом принимает кэш третьего уровня. Чем больше размер кэша третьего уровня, тем реже он обращается к памяти ОЗУ.
А при чём здесь планирование потоков? А вот причем! Каждый раз, когда планировщик решает разбудить поток, он принимает решение о том, на каком ядре процессора поток будет запущен. Если свободно ядро, на котором поток работал до перевода в спящий режим, то поток (вероятнее всего) продолжит работать на том же ядре. В этом случае операция восстановления контекста потока будет наиболее дешёвой, поскольку вся необходимая информация скорее всего осталась в кэше первого уровня ядра. Если это ядро занято, то планировщик попытается найти другое свободное ядро процессора. Но в его кэше первого уровня отсутствуют данные, необходимые для восстановления контекста потока, поэтому потребуется больше времени для возобновления работы потока.
В том случае, если планировщик не нашёл свободных ядер, то он анализирует приоритеты потоков. Если он обнаружит, что на одном из ядер выполняется поток с меньшим приоритетом, то он может прервать работу такого потока и запустить на этом ядре поток с большим приоритетом.
-->
# 6.3 Приоритеты потоков в Windows <a name="about_priority_in_windows"></a>
При разработке многопоточной программы для Windows существует возможность назначать потоку уровень приоритета (свойство `TThread.Priority`). По умолчанию используется уровень приоритета `tpNormal`. Тип `TThreadPriority` объявлен следующим образом:
```pascal
{$IFDEF MSWINDOWS}
TThreadPriority = (tpIdle, tpLowest, tpLower, tpNormal, tpHigher, tpHighest,
tpTimeCritical) platform;
{$ENDIF MSWINDOWS}
```
Обычно программисту нет смысла изменять уровни приоритетов потоков. Приоритет потока никак не влияет на скорость исполнения программного кода. Приоритет потока никак не влияет на размер кванта времени (однако размер кванта времени зависит от того, является ли приложение активным или нет).
:information_source: **Внимание!** У приложения, которое находится на переднем плане, длительность кванта времени увеличивается примерно в 3 раза. В том случае, если два приложения будут привязаны к одному ядру процессора, то одну и ту же вычислительную задачу быстрее (примерно в 3 раза) сможет решить приложение, которое находится на переднем плане. На моих компьютерах с Windows 7 длительность кванта времени у приложений на заднем плане (думаю, что и у служб тоже) составляет 32 мс, а у приложений на переднем плане - 96 мс.
Уровень приоритета потока влияет на выделение процессорного времени как между потоками в рамках одного процесса, так и между потоками различных процессов.
Изменять уровень приоритета потока не имеет смысла, если выполняется задача, в которой основное время уходит на ожидание какого-либо события.
Если в Вашей программе работают 2 потока, у одного уровень приоритета `tpNormal`, а у второго `tpLower` и оба потока привязаны к одному и тому же ядру процессора, то первому потоку гораздо чаще будет предоставляться процессорное время (например, 98%). С другой стороны, если у этих же потоков не будет привязки к одному и тому же ядру, то они получат одинаковое процессорное время. Скорее всего, оба варианта – это не то, чего Вы хотите добиться при изменении уровня приоритета потока.
Существует 2 крайних уровня приоритетов: `tpIdle` и `tpTimeCritical`. Поток с приоритетом `tpIdle` получит процессорное время только в том случае, если нет других активных потоков с более высоким приоритетом. Поток с приоритетом `tpTimeCritical` будет забирать себе всё процессорное время, т.е. потоки с более низким приоритетом не получат квант времени, если выполняется поток с приоритетом `tpTimeCritical`. Это верно для одноядерного процессора. Если процессор многоядерный (сейчас это обычная ситуация), то, скорее всего, будут выполняться одновременно и поток с приоритетом `tpIdle` и поток с приоритетом `tpTimeCritical`.
:information_source: **Внимание!** *В каталоге CalcTimeQuant находится программа, позволяющая производить эксперименты с приоритетами и замеры длительности квантов времени.*
:information_source: Помимо терминов "уровень приоритета потока" и "класс приоритета процесса", существует также термин "базовый приоритет потока". Как определить базовый приоритет потока, смотрите в [таблице приоритетов](https://docs.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities).
# 7. Где можно и где нельзя создавать и уничтожать потоки <a name="bad_places_for_create_free_threads"></a>
Данные замечания актуальны при разработке программ под Windows. Существует ряд мест, где не следует создавать либо уничтожать потоки.
1. Создавать потоки не следует в секции `initialization` (как в EXE, так и в DLL). По моему опыту создание дополнительных потоков в секции `initialization` EXE-модуля (не важно, в каком pas-файле) приводит к снижению стабильности приложения. Грубо говора, один раз из 100 возникает сбой при запуске приложения.
2. Не следует уничтожать потоки в секции `finalization` DLL-библиотеки. Если Вы попытаетесь это сделать, то, скорее всего программа зависнет (это происходит из-за особенностей выгрузки DLL-библиотек в ОС Windows).
Если Вы разрабатываете DLL-библиотеку, в которой используются дополнительные потоки, то рекомендую реализовать в ней две экспортные функции – одну для создания необходимых объектов в библиотеке (например, `CreateLibrary`), вторая для уничтожения созданных объектов (например, `DestroyLibrary`). В этом случае код создания потоков можно разместить в `CreateLibrary` (либо создавать их позже), а код уничтожения объектов потоков разместить в `DestroyLibrary`.
2020-07-31 22:49:03 +03:00
# 8. Немного о threadvar <a name="about_threadvar"></a>
2020-08-03 08:43:44 +03:00
При разработке многопоточного приложения в Delphi программист может использовать ключевое слово `threadvar` (локальная переменная потока) при объявлении глобальных переменных. Для каждого дополнительного потока создаётся собственная копия таких переменных. Один поток не сможет прочитать значение, которое записал в эту переменную другой поток. Переменные, объявленные в `threadvar`, создаются при создании дополнительного потока, а уничтожаются при завершении работы потока.
2020-07-31 22:49:03 +03:00
:warning: **Внимание!** *Обычно Вам не требуется использовать `threadvar` при объявлении глобальных переменных.* Если Вам нужно объявить переменные, к которым будет иметь доступ только один дополнительный поток, объявите их в секции private вашего класса (наследника от `TThread`) (мы так уже многократно делали в предыдущих примерах). Для уместного использования `threadvar` необходимо иметь действительно очень вескую причину!
2020-08-03 08:43:44 +03:00
:information_source: **Информация!** На мой взгляд, наиболее ярким примером уместного использования `threadvar` является UniGui - классный фреймворк, позволяющий разрабатывать мощные, масштабные "десктопные" приложения, которые работают в браузере. При разработке программы на UniGui код по стилю написания очень похож на обычный Delphi-код для десктопной программы. Однако каждый запрос пользователя (а пользователей может работать одновременно несколько сотен) обрабатывается в отдельном потоке. Для каждого пользователя в момент обращения к UniGui-приложению с помощью браузера создаётся необходимый набор форм, модулей данных, компонентов и всего остального таким образом, что пользователь, не догадывается, что одновременно с ним работают ещё сотни других пользователей.
2020-07-31 22:49:03 +03:00
Вот типичный код получения ссылки на объект формы и модуля данных в UniGui:
```pascal
function MainmForm: TMainmForm;
begin
Result := TMainmForm(UniMainModule.GetFormInstance(TMainmForm));
end;
function UniMainModule: TUniMainModule;
begin
Result := TUniMainModule(UniApplication.UniMainModule)
end;
```
Это обычные функции! Они не являются методами какого либо класса. Однако мы можем смело обращаться к форме `MainmForm` либо к модулю данных `UniMainModule` из любой другой формы. Мы можем из обработчика OnClick кнопки, лежащей на форме `MainmForm`, обратиться с компоненту `TDataSet`, который находится в модуле данных `UniMainModule`.
А теперь подумайте, сколько в программе создано форм `MainmForm` и модулей данных `UniMainModule`, если работают одновременно 1000 пользователей? Правильно, может быть создано 1000 форм и 1000 модулей данных. Так как же UniGui понимает, какой `TUniMainModule` нужно вернуть если функция `UniMainModule` объявлена без параметров? Вдруг будет возвращён экземпляр `TUniMainModule` от чужой сессии, в которой работает другой пользователь?
2020-08-03 08:43:44 +03:00
Ответ прост: функция `UniApplication` возвращает соответствующую переменную класса `TUniGUIApplication`, объявленную в секции `threadvar`. При обработке запроса от пользователя:
2020-07-31 22:49:03 +03:00
1. UniGui создаёт (либо получает из пула) поток, который будет отвечать за обработку запроса, пришедшего от браузера;
2020-08-03 08:43:44 +03:00
2. Поток анализирует запрос, извлекает из него идентификатор сессии и отыскивает объект `TUniGUIApplication`, который соответствует данному идентификатору сессии;
3. Поток сохраняет найденный объект `TUniGUIApplication` в переменной, объявленной в секции `threadvar`.
4. Поток запускает необходимые обработчики событий (например, OnClick). Из этих обработчиков событий можно смело вызывать функции `UniApplication`, `UniMainModule`, `MainmForm` и т.д.
2020-07-31 22:49:03 +03:00
2020-08-15 22:42:42 +03:00
:warning: **Внимание!** Следует быть осторожным при объявлении в секции `threadvar` некоторых типов (строки, динамические массивы, варианты, интерфейсы). Причины этого хорошо описаны в [официальной документации](http://docwiki.embarcadero.com/RADStudio/Rio/en/Variables_(Delphi)#Thread-local_Variables).
2020-07-31 23:59:46 +03:00
<!--
:warning: **Внимание!**
:exclamation: **Внимание!**
:information_source: **Информация!**
-->