From 4bfa10e443f51081bcdb49350edd48d9dc3aab67 Mon Sep 17 00:00:00 2001 From: loginov-dmitry <67510034+loginov-dmitry@users.noreply.github.com> Date: Sun, 31 Jan 2021 18:50:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20"=D0=A0=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20=D0=B1=D0=B0=D0=B7=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- multithread_in_delphi_for_beginners.md | 80 +++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/multithread_in_delphi_for_beginners.md b/multithread_in_delphi_for_beginners.md index 4c59ba5..609de61 100644 --- a/multithread_in_delphi_for_beginners.md +++ b/multithread_in_delphi_for_beginners.md @@ -1,6 +1,6 @@ # Многопоточное программирование в Delphi для начинающих -(редакция 1.7 от 14.01.2021г) +(редакция 1.8 от 31.01.2021г) Автор: Логинов Д.С. Пенза, 2021 @@ -12,13 +12,10 @@ markdown-редакторы не подошли, т.к. они либо не у Возможные темы для дальнейшей работы над статьёй: - передача данных в доп. поток: использование очереди Windows -- передача данных в доп. поток: использование очереди RTL - передача данных в доп. поток: использование очереди TThreadedQueue - many consumers - передача данных в доп. поток: пример организации записи в лог на удалённый сервер: множество потоков генерируют события, один поток передаёт на сервер через одно подключение - можно попробовать наглядно показать преимущество использования Эвента по сравнению со Sleep(20). -- работа с базами данных: каждый поток должен обращаться к БД через своё подключение. Однако могут быть компоненты, - которые поддерживают множество подключений к БД через одно TCP-соединение, учесть это как-то! - пример навешивания модального окна ожидания окончании операции в доп. потоке - пример создания анонимного потока в современных Delphi - пример программирования в PPL. Нужно дать ссылки на статьи и видеоуроки в интернете @@ -106,16 +103,18 @@ markdown-редакторы не подошли, т.к. они либо не у 9. Передача данных в дополнительный поток [...](#send_data_to_thread) -9.1 Использование обычного списка (TList, TStringList, TThreadList) и периодический контроль списка [...](#send_data_to_thread_sleep) + 9.1 Использование обычного списка (TList, TStringList, TThreadList) и периодический контроль списка [...](#send_data_to_thread_sleep) -9.2 Использование обычного списка (TList, TStringList, TThreadList) и объекта Event [...](#send_data_to_thread_Event) + 9.2 Использование обычного списка (TList, TStringList, TThreadList) и объекта Event [...](#send_data_to_thread_Event) -9.3 Использование очереди `TThreadedQueue` (множество producer-потоков и один consumer-поток) [...](#threaded_queue_one_consumer) + 9.3 Использование очереди `TThreadedQueue` (множество producer-потоков и один consumer-поток) [...](#threaded_queue_one_consumer) 10. Где можно и где нельзя создавать и уничтожать потоки [...](#bad_places_for_create_free_threads) 11. Немного о threadvar [...](#about_threadvar) +12. Работа с базой данных из дополнительного потока [...](#work_with_db) + # 1. Вступление @@ -193,6 +192,18 @@ Delphi – это прежде всего инструмент для разра Кстати, операция переключения контекста по времени далеко не бесплатная, поэтому производители процессоров и разработчики ОС постоянно делают улучшения для повышения скорости данной операции. Если Вы хотите создать эффективный высокопроизводительный WEB-сервер, то будет не лишним подумать над тем, как минимизировать количество переключений контекста при исполнении Вашего кода. *Я не буду давать рекомендаций по поводу создания высокопроизводительного WEB-сервера, поскольку задача эта больше подходит для специализированных языков программирования WEB-серверов, например GoLang, а Delphi заточен в первую очередь на разработку фронтэнда, а также корпоративного программного обеспечения.* + + # 2. Базовый класс многопоточности - TThread `TThread` – это базовый класс, инкапсулирующий функционал для обеспечения работы параллельного потока. Если мы хотим реализовать код, который должен выполняться в отдельном потоке, то нам необходимо реализовать наследника от класса `TThread` и, как минимум, реализовать override-метод Execute, который перекроет виртуальный абстрактный метод `Execute`, объявленный в родительском классе `TThread`. Код, находящийся внутри метода `Execute` будет исполняться в отдельном потоке. @@ -2378,9 +2389,9 @@ end; Delphi-программисту для работы с семафором доступны API-функции Windows: CreateSemaphore, OpenSemaphore, ReleaseSemaphore, WaitForSingleObject, WaitForMultipleObjects и т.д. Также доступы кроссплатформенные класс-обёртка TSemaphore и класс TLightweightSemaphore (легковесный семафор) из модуля "System.SyncObjs". -За многие десятилетия было опубликовано множество научных работ, в которых показано, как реализовать тот или иной алгоритм синхронизации на основе семафора. К сожалению, с практической точки зрения есть трудности: весьма сложно понять, для чего нужно применять семафор непосредственно программисту при решении прикладных задач (вероятно, в будущем у меня появятся удачные примеры использования семафоров). Понятно, что семафор позволяет ограничить количество потоков, которые одновременно работают над выполнением какой-то задачи. Но на практике получается обычно наоборот - один поток выполняет множество задач (по очереди). Пишут либо про некие абстрактные "ресурсы", доступ к которым контролируется с помощью семафора, либо знакомят с проблемой взаимоблокировок и с её решением (см. "Обедающие философы"), либо чрезмерно детально описывают решение некоторой прикладной задачи, в которой смогли удачно вписать использование семафора. + -К сожалению, я не решал задач, которые могли бы продемонстрировать возможности семафоров при разработке прикладного ПО. К тому же я не собирался в рамках данной статьи (для начинающих) демонстрировать сложные алгоритмы синхронизации между потоками. +:information_source: **Конкретный пример использования семафора.** Есть множество клиентов, которые формируют отчёт через веб-интерфейс. Есть сервер, который обрабатывает запросы от браузеров, формирует отчёт и возвращает результат. Возможности сервера ограничены. Если он будет формировать одновременно более 10 отчётов, то столкнётся с аппаратными ограничениями компьютера (объём ОЗУ, количество ядер процессора, скорость накопителя). Например, при формировании 20 отчётов 32-битный процесс сервера упрётся в ограничение виртуального адресного пространства (2 ГБ). Если количество ядер недостаточно, то может быть другая ситуация: сервер будет слишком медленно выполнять другие задачи (например, обмен с торговыми точками или майнинг криптовалюты :). Все эти проблемы легко решить с помощью семафора: создаём семафор и указываем ограничение на 10 потоков. ## 7.5 Критическая секция и монитор @@ -2521,6 +2532,57 @@ end; :warning: **Внимание!** Следует быть осторожным при объявлении в секции `threadvar` некоторых типов (строки, динамические массивы, варианты, интерфейсы). Причины этого хорошо описаны в [официальной документации](http://docwiki.embarcadero.com/RADStudio/Rio/en/Variables_(Delphi)#Thread-local_Variables). +# 12. Работа с базой данных из дополнительного потока + +Лично я не рекомендую работать с базой данных из десктопного приложения напрямую. Намного перспективнее разработать, как сейчас модно говорить, "микросервис", который: +а) будет находиться на том же компьютере, что и база данных (либо на соседнем сервере); +б) сможет работать с базой данных по наиболее быстрому протоколу с минимальными сетевыми задержками; +в) сможет принимать от клиентов с помощью JSON-запросов команды на запрос данных и изменение данных. При этом клиентскому приложению вообще необязательно использовать сущность "таблица базы данных", оно может оперировать понятием "объект бизнес-логики". Какие именно таблицы в БД используются для хранения данных объекта бизнес-логики, решает исключительно микросервис. Благодаря такому подходу значительно увеличивается производительность системы по сравнению с работой с базой данных напрямую, особенно в тех случаях, когда клиентское приложение находится на значительном удалении от базы данных, т.е. имеются высокие сетевые задержки; + +:information_source: **Информация!** Если Вы до сих пор не работали с JSON и не знаете, что это такое, советую обязательно этот пробел заполнить, т.к. на текущий момент JSON является общепринятым форматом организации, хранения и передачи информации (как когда-то XML, только проще и удобнее). + +Использование вспомогательного микросервиса: +а) позволит повысить безопасность базы данных (TCP-порт для подключения к базе данных выводить наружу не требуется); +б) позволит использовать любую бесплатную базу данных (SQLite, Firebird, PostgreSQL и др., а также NoSql базы данных), при этом приложению-клиенту даже не нужно знать, какая база данных используется; +в) позволит изменять правила обработки бизнес-логики без необходимости обновления приложений-клиентов; + +В любом случае, каким бы образом Вы не работали с базой данных (напрямую из десктопного приложения или через вспомогательный микросервис), Вам никуда не деться от использования дополнительного потока для работы с базой данных. Конечно, в простейших случаях в десктопном приложении Вы можете всё общение с базой данных организовать из основного потока. Более того, некоторые компоненты умеют работать с базой данных в асинхронном режиме. Некоторые компоненты умеют показывать на экране вспомогательную модальную форму, которая появляется каждый раз, когда выполняется обращение к базе данных. Но давайте посмотрим недостатки этих подходов: + +а) при работе с БД напрямую из основного подхода интерфейс пользователя будет тормозить в тех случаях, если выполняется "тяжелый" запрос, либо имеются очень длительные сетевые задержки; +б) использование асинхронного режима может очень сильно запутать программу. В такой программе будет очень сложно (иногда даже невозможно) разбираться. Хорошо, если компонент доступа к БД позволяет навесить обработчик события получения данных (например, OnDataLoaded) в стиле анонимной процедуры, иначе, при использовании обычных методов, придётся как-то организовывать сохранение контекста обработки данных; +в) если при каждом запросе показывать на экране вспомогательную форму (или даже курсор с песочными часиками), то пользователям будет неприятно пользоваться такой программой. + +Если программа достаточно навороченная, то у неё должны быть различные автоматические операции, выполняемые по расписанию, либо через заданный период времени, например: + +а) автоматическая передача последних накопленных данных в центральную базу данных (например, из точек продаж, либо из филиалов большой организации); +б) выгрузка данных для их дальнейшей обработки в 1С или иной внешней системе; +в) обработка файла с данными, подготовленными во внешней системе; +г) автоматическая передача данных в облачный сервис (например, передача последних пробитых чеков в ОФД); +д) и бесконечное множество других вариантов. + +Надеюсь, читателю теперь стало очевидно, что очень важно уметь работать с базой данных из дополнительного потока. + +:information_source: **Рекомендация.** Я рекомендую все автоматические операции с базой данных выполнять **только** из дополнительного потока. В идеале, как я считаю, в программе не должно быть ни одного таймера с обработчиком события OnTimer, в котором бы выполнялись обращения к базе данных через главный компонент подключения, используемый в десктопном приложении. Такой подход очень помогает в тех случаях, когда происходит разрыв подключения к БД. Иначе, если разрыв подключения к БД произошёл, то при каждом срабатывании обработчика OnTimer мы будем иметь подвисание интерфейса программы (из-за попытки установить новое сетевое подключение к БД), либо каждый раз на экране будет выдаваться новое окно с сообщением об ошибке. Будет лучше, если подобные ошибки будут возникать в дополнительных потоках и фиксироваться в логах, а у пользователя останется возможность работать с программой и выполнять те операции, которые не связаны с базой данных. Можно даже отобразить на экране отдельное окно с кнопкой "Выполнить попытку подключиться к БД". + +:warning: **Внимание!** Вы не можете создать в программе единственный компонент подключения к базе данных (например, TIBDataBase) и использовать его для работы с базой данных одновременно из нескольких потоков. + +Причины этого следующие: + +а) некоторые компоненты подключения к БД не рассчитаны на одновременную работу с ними из нескольких потоков (в том числе TIBDataBase). Внутри компонентов находятся различные списки, например список подключенных объектов TIBDataSet или список транзакций. Эти списки не всегда защищены от одновременного доступа; +б) компонент подключения устанавливает соединение с сервером управления базы данных (СУБД) (например, по протоколу TCP/IP) и дальнейний обмен с СУБД осуществляется по принципу "запрос / ответ". Если один поток выполнил запрос к БД, то компонент подключения передаёт информацию в СУБД и ждёт ответа от СУБД. До тех пор, пока не будет ответа от СУБД, компонент подключения не будет передавать в СУБД новые запросы, даже если они поступают из других потоков. Таким образом, нет никакого смысла пытаться работать с базой данных из нескольких потоков с использованием одного компонента подключения. + +:information_source: **Информация!** Очень важно при многопоточной работе с БД иметь пул подключений. Пул подключений - это по сути массив (список), в котором хранятся активные подключения к БД. Благодаря использованию пулов Вы экономите значительное время на процедуру установки соединения с БД, особенно если используется TLS-шифрование канала связи. Если Вы используете компоненты IBX для работы с Firebird, то можете использовать пул подключений из библиотеки [ibxFBUtils](https://github.com/loginov-dmitry/ibxfbutils). Данный пул я использую в различных коммерческих проектах более 10 лет, поэтому можно быть уверенным, что в нём всё вылизано досконально. + +Итак, типовая работа с базой данных в дополнительном потоке выглядит следующим образом: + +1. Создаём объект подключения либо берём готовое подключение из пула; +2. Создаём объект-транзакцию (при необходимости); +3. Выполняем необходимые запросы к базе данных; +4. Коммитим транзакцию (если была произведена модификация данных в БД); +5. Уничтожаем объект подключения либо возвращаем его обратно в пул. + + +