rzaripov.kz

Безопасная многопоточность в FireMonkey-приложениях

FireMonkey позволяет создавать приложения для Windows, macOS, Linux, Android и iOS из общей кодовой базы. Такая универсальность особенно полезна для задач, где интерфейс должен отображать данные из сети, базы данных, файловой системы или внешнего сервиса. Однако перенос длительной операции в фоновый поток сам по себе не делает приложение безопасным: важно правильно разделить работу с данными и взаимодействие с визуальными компонентами.

Главное правило Delphi и FireMonkey заключается в том, что элементы пользовательского интерфейса принадлежат главному потоку. Фоновая задача может загружать данные, выполнять расчёты и обращаться к серверу, но изменение TLabel, TListView, TProgressBar или других контролов должно выполняться через механизм синхронизации с главным потоком.

Почему интерфейс должен оставаться в главном потоке

Главный поток отвечает за обработку событий, перерисовку формы, жесты, клавиатуру и внутренний цикл приложения. Если в обработчике кнопки выполняется большой SQL-запрос, распаковка архива или загрузка нескольких мегабайт данных, цикл сообщений временно блокируется. Пользователь видит зависшее окно, хотя процесс технически продолжает работать.

Попытка напрямую изменить контрол из фонового потока создаёт другую проблему. Некоторые операции могут завершиться без видимых последствий, но это случайное поведение нельзя считать безопасным. При одновременной перерисовке и изменении свойств возможны исключения, повреждение состояния компонента и ошибки, которые сложно воспроизвести.

Полезно мысленно разделять приложение на три уровня: интерфейс, прикладную логику и источник данных. Например, код подключения к MySQL не должен знать о конкретной кнопке формы, а обработчик формы не должен самостоятельно управлять каждым шагом сетевого запроса. Такой подход хорошо согласуется с рекомендациями из материала о практиках работы с MySQL, где важны разделение ответственности и корректная обработка ошибок.

TThread, TTask и фоновые операции

Классический вариант для Delphi — создать наследника TThread и реализовать работу в методе Execute. Такой подход удобен для сложного процесса с несколькими этапами, отменой, собственным состоянием и чётким жизненным циклом. В конструкторе потоку обычно передают True, чтобы сначала настроить объект, а затем запустить его вызовом Start.

Для коротких независимых операций подходит TTask.Run из Parallel Programming Library. Задача получает анонимную процедуру и выполняется в пуле потоков. Это уменьшает количество шаблонного кода, но требует внимательного отношения к захваченным переменным и времени жизни объектов формы.

TTask.Run(
  procedure
  var
    Text: string;
  begin
    Text := LoadFromServer;
    TThread.Queue(nil,
      procedure
      begin
        StatusLabel.Text := Text;
      end);
  end);

В этом примере загрузка выполняется вне UI-потока, а присваивание свойству StatusLabel.Text переносится в главный поток. Для короткого обновления TThread.Queue обычно предпочтительнее Synchronize, поскольку вызывающий поток не блокируется в ожидании завершения пользовательского кода.

Synchronize, Queue и передача результата

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

TThread.Queue ставит процедуру в очередь главного потока и позволяет рабочему потоку продолжить выполнение. Такой механизм хорошо подходит для прогресса, промежуточных сообщений и публикации готового результата. Для вызова из уже главного потока следует учитывать особенности Queue; в некоторых сценариях используется TThread.ForceQueue, чтобы процедура гарантированно прошла через очередь.

Передавать в UI-поток лучше готовые значения, а не рабочие объекты. Например, поток формирует строку, массив записей или отдельную модель данных, после чего интерфейс получает неизменяемую копию. Нельзя передавать компонент формы, соединение с базой или поток, который продолжает изменяться одновременно с чтением.

Ошибки также должны возвращаться в главный поток. Исключение, возникшее внутри TTask, не следует оставлять без обработки: его можно перехватить в фоновой процедуре, сохранить текст и передать в очередь. Тогда форма покажет понятное сообщение, а приложение не столкнётся с неожиданным завершением задачи.

Защита общих данных

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

Для защиты критического участка применяют TCriticalSection, TMonitor, TMutex или атомарные операции TInterlocked. Блокировка должна быть короткой: поток захватывает ресурс, быстро читает или изменяет данные и освобождает блокировку. Ожидание сети, диска или другого потока внутри критической секции обычно является плохим решением.

Lock.Acquire;
try
  LocalValue := SharedValue;
finally
  Lock.Release;
end;

Важно заранее определить владельца данных. Если фоновая задача полностью владеет рабочим массивом, интерфейс получает его копию после завершения обработки. Если же один объект совместно используется несколькими потоками, для каждого изменяемого поля нужны понятные правила чтения и записи. Блокировка «на всякий случай» часто приводит к взаимным блокировкам и усложняет сопровождение.

Отмена, завершение и время жизни формы

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

Для отмены можно использовать флаг, защищённый атомарной операцией, или механизм TEvent. Долгая операция должна периодически проверять признак отмены между этапами: после чтения блока файла, получения страницы, обработки группы записей. Внешний вызов, который сам не поддерживает отмену, всё равно следует обернуть проверкой до и после его выполнения.

При закрытии формы нужно запретить новые задачи, запросить отмену текущих и дождаться корректного завершения либо гарантировать, что завершившийся поток больше не обращается к интерфейсу. Анонимная процедура, захватившая Self, способна продлить жизнь формы или сохранить опасную ссылку. Надёжнее передавать отдельный объект состояния и проверять, существует ли получатель результата.

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

Практический шаблон для FireMonkey

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

Для кроссплатформенного проекта полезно изолировать платформенные детали. Доступ к файлам, уведомлениям и сетевым настройкам может различаться в Windows, Android и macOS, тогда как контракт фоновой службы остаётся единым. Такой стиль пригодится и в проектах, связанных с разработкой программных решений для среды KAZVT, где важны переносимость и стабильность поведения.

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

Механизм Когда использовать Главное ограничение
TThread Сложный процесс с отменой и состоянием Требует ручного управления жизненным циклом
TTask.Run Короткая независимая фоновая операция Нужно контролировать захваченные объекты
TThread.Queue Асинхронное обновление UI Код выполняется позже, состояние могло измениться
TThread.Synchronize Строгое ожидание действия UI Блокирует рабочий поток
TCriticalSection Защита общего ресурса Нельзя удерживать блокировку во время долгих операций
TEvent Сигнализация и отмена Требует корректного ожидания и освобождения

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