Многопоточность

Кому и зачем нужна многопоточность в Delphi: разбор целевых сегментов
Многопоточность — это не абстрактная фича, а инструмент решения конкретных бизнес-проблем. В 2026 году разработчики на Delphi сталкиваются с тремя основными сценариями: обработка больших массивов данных (финансовые расчёты, логистика), работа с внешними API и базами данных без зависания интерфейса (ERP, CRM), а также параллельная обработка потоков данных (аналитика, отчёты). Каждый сценарий требует своего подхода. TThread — для низкоуровневого контроля и постоянных фоновых задач. TTask (PPL) — для быстрой распараллеливания вычислительных операций. Parallel.For — идеален для обработки коллекций без ручного управления потоками.
Выбор технологии напрямую влияет на стабильность приложения. Например, для GUI-приложений критично делать вызовы из главного потока через Synchronize или Queue — иначе получите deadlock или Access Violation. Для серверных служб без интерфейса можно смело использовать асинхронные операции с IAsyncResult и перекрывающиеся события. Ниже разберём каждый инструмент с цифрами и конкретными настройками.
Кейс «Большой отчёт за 5 секунд»: Setup, проблема и решение
Клиент: компания, разрабатывающая модуль финансового учёта для 1С-подобной системы на Delphi. Проблема: генерация сводной таблицы по 50 000 транзакциям занимала 42 секунды. Всё это время интерфейс висел, пользователи нажимали Alt+F4 и теряли данные. Было две тысячи активных пользователей — 42 секунды × 2000 = 23 часа простоя в день.
Решение: Заменили синхронный цикл по записям на TTask.WaitForAll с ручным разделением данных на 8 чанков (по числу ядер процессора). Каждый чанк обрабатывался в отдельном Task, затем результаты объединялись. Для синхронизации доступа к общему списку использовали TMemoryStream.Lock (легче, чем TCriticalSection). После завершения всех задач — один вызов Synchronize для обновления StringGrid. Ключевой параметр: количество Task равно числу ядер (Environment.ProcessAffinityMask), чтобы избежать переключения контекста.
Результат: время генерации отчёта упало с 42 до 5,3 секунды — в 8 раз быстрее. Интерфейс не зависал, потому что обновление GUI происходит только один раз. Пользователи перестали закрывать приложение. Дополнительно: удалось отказаться от отдельного сервера отчётов, сэкономив $300/мес на аренде.
- Инструмент: TTask и TParallel.For из Parallel Programming Library (PPL) — встроено в Delphi XE7+ и актуально для 2026 версий.
- Ключевой параметр: количество параллельных задач = LogicalCPUCount — 1 (один поток оставляем для системных нужд).
- Синхронизация: только один раз в главном потоке после завершения всех задач — это исключает race condition.
- Ошибка, которую исправили: ранний код использовал TList
без блокировок — при параллельной записи возникали дубли и пропуски строк. - Метрика успеха: время обработки 50 000 записей — менее 6 секунд (измеряли TStopwatch.GetTimestamp).
TThread vs TTask vs TParallel.For: когда что выбирать
Хотя все три класса решают одну задачу, у каждого есть оптимальная ниша. TThread нужен, когда требуется долгоживущий поток с собственным циклом (например, мониторинг папки или COM-порта). Вы сами управляете его жизненным циклом, приоритетом и обработкой исключений. Недостаток: приходится писать много вспомогательного кода (Sync, Queue, Terminated). В 2026 году TThread остаётся актуальным для встраиваемых систем и legacy-кода.
TTask (System.Threading) — это управляемая абстракция. Вы создаёте задачу, и пул потоков (ThreadPool) сам решает, на каком ядре её выполнять. Идеально для асинхронных вызовов: «скачай данные, обработай, отобрази». Не требует ручного уничтожения. TTask.Future позволяет получить результат без явной синхронизации. Узкое место: если задача зависнет, ThreadPool может истощиться.
TParallel.For — это макрос для параллельного цикла. Внутри он сам разбивает итерации на бэтчи и распределяет по потокам. Лучший выбор, когда вам нужно применить одну и ту же функцию к элементам массива или списка. Работает только для независимых итераций (без shared state без блокировок). Скорость прироста — практически линейная до 8 потоков, затем эффект снижается из-за кэш-промахов.
- Для GUI-приложений с фоновыми задачами — используйте TTask и Synchronize в конце. TThread избыточен.
- Для высоконагруженных серверов без интерфейса — TTask + TThreadPool.SetMaxWorkerThreads.
- Для пакетной обработки массивов данных — TParallel.For с локальными инициализаторами (TLocalResult).
- Для работы с COM и OLE (Excel, Word) — только TThread с CoInitialize/CoUninitialize внутри Execute.
- Для задач с приоритетами (логирование vs. расчёт) — TThread с SetPriority, так как TTask не умеет приоритеты.
Практические инструменты: какие параметры и методы реально работают
Начнём с TTask. Главный метод — TTask.WaitForAll(TTaskArray) — он блокирует поток, пока все задачи не выполнятся. Если не хотите блокировать GUI, используйте TTask.Run и внутри последней задачи вызывайте TThread.Queue(nil, UpdateUI). Важный нюанс: TTask.Запуск в Delphi 11+ работает с TAsyncResult, что позволяет отменять задачи через CancellationToken. Для этого передаёте сигнальный объект TInstantCancellationToken — это нововведение 2025-2026 годов, но в Delphi 11 оно уже реализовано через TTaskCancelFunc.
Для TParallel.For критичен параметр Parallel.Aggressive: если он включён (по умолчанию), то распараллеливаются даже короткие циклы (от 10 итераций). Для длинных циклов (1000+ итераций) лучше выключить агрессивный режим, чтобы не тратить время на создание потоков. Структура цикла: TParallel.For(0, High(Data), procedure(i: Integer; var DoBreak: Boolean) ... ). Внутри можно использовать TLocalData для накопления промежуточных результатов — это снижает contention на shared переменных.
TThread остаётся незаменимым для тонкой работы с ресурсами. Метод SetThreadExecutionState предотвращает гибернацию во время расчётов — актуально для ноутбуков. Рекомендуется создавать поток через TThread.CreateAnonymousThread для быстрых задач, и через наследник для сложных. Не забывайте FreeOnTerminate := True — иначе утечка памяти гарантирована. Для синхронизации используйте TCriticalSection, TMonitor или TEvent (событие). Избегайте Synchronize внутри цикла — это убивает всю параллельность.
- Пример настройки TParallel.For: TParallel.AsyncFor(0, 999999, Params, ProcessFunc) — где Params содержит число потоков (ThreadPoolSize).
- Как отменять TTask: создайте TTask.Create(Proc, CancellationToken). Или используйте TTask.Run(Proc, CancellationToken).
- Лучшая практика блокировки: вместо TCriticalSection используйте TMonitor.Enter/Exit — он встроен в любой объект и быстрее.
- Избегайте TThread.Queue вызовов внутри TParallel.For — это вызывает deadlock при заполнении очереди сообщений.
- Для диагностики: TThread.Current.ThreadID + TStopwatch.GetTimestamp дают точное время выполнения каждого потока.
Типичные ошибки и как их избежать (с примерами кода)
Ошибка 1: Гонка данных при параллельном чтении/записи в TStringList. Решение: используйте TStringList.Lock (если это TThreadStringList) или разделите данные на независимые сегменты. Fabric: 100% race condition приведёт к AV. Лучше весь вывод накапливать в локальном списке и объединять в критической секции.
Ошибка 2: Слишком много потоков (500 потоков для 4-ядерного CPU). Решение: ограничьте число потоков через TThreadPool.Default.SetMaxWorkerThreads(CPUCount * 2). Идеальное число — количество логических ядер. Превышение ведёт к переключению контекста — падение производительности до 40%.
Ошибка 3: Блокировка главного потока внутри TTask. Решение: никогда не вызывайте TTask.WaitForAll в главном потоке. Используйте продолжение (Continuation) через TTask.Completed или TTask.Future. Вместо блокировки подписывайтесь на событие OnTerminated. Это критично для Delphi-приложений с FireMonkey (Android, iOS) — там блокировка приводит к сбою сенсорного ввода.
Вывод: как выбрать свой путь в многопоточности Delphi 2026
Для бытового выбора достаточно трёх вопросов: (1) «Работаю ли я с GUI?» — да = TTask + Synchronize; (2) «Нужен ли мне поток-демон с постоянным опросом?» — да = TThread; (3) «Обрабатываю массив данных 1000+ элементов?» — да = TParallel.For. Для серверных служб, не связанных с GUI — смело комбинируйте TTask и Parallel.For, ограничивая пул потоков. Категорически не используйте TThread в новых проектах, если нет legacy-обоснования — TTask современнее, безопаснее и производительнее за счёт ThreadPool.
Проверенный в 2026 году рецепт для типовой задачи «загрузить данные из БД, обработать, вывести в отчёт»: TTask.Run → внутри TParallel.For для трансформации → TThread.Queue для обновления GUI. Все блокировки — только TMonitor, и только на этапе финального сбора. Это даёт прирост скорости от 4 до 10 раз в зависимости от размера данных и числа ядер. Избегайте ранней оптимизации: сначала замерьте TStopwatch профилировщиком (например, AQTime или встроенный Profiler в Delphi 12), потом решайте, добавлять ли многопоточность. Если обработка занимает менее 100 мс — однопоточный код часто оказывается быстрее из-за накладных расходов на создание потоков.
Добавлено: 27.04.2026
