mirror of
https://github.com/loginov-dmitry/multithread.git
synced 2025-02-20 07:58:22 +02:00
Добавлен раздел "Работа с базой данных из дополнительного потока"
This commit is contained in:
parent
645de93694
commit
4bfa10e443
@ -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<T>` (множество producer-потоков и один consumer-поток) [...](#threaded_queue_one_consumer)
|
||||
9.3 Использование очереди `TThreadedQueue<T>` (множество producer-потоков и один consumer-поток) [...](#threaded_queue_one_consumer)
|
||||
|
||||
10. Где можно и где нельзя создавать и уничтожать потоки [...](#bad_places_for_create_free_threads)
|
||||
|
||||
11. Немного о threadvar [...](#about_threadvar)
|
||||
|
||||
12. Работа с базой данных из дополнительного потока [...](#work_with_db)
|
||||
|
||||
|
||||
# 1. Вступление <a name="intro"></a>
|
||||
|
||||
@ -193,6 +192,18 @@ Delphi – это прежде всего инструмент для разра
|
||||
|
||||
Кстати, операция переключения контекста по времени далеко не бесплатная, поэтому производители процессоров и разработчики ОС постоянно делают улучшения для повышения скорости данной операции. Если Вы хотите создать эффективный высокопроизводительный WEB-сервер, то будет не лишним подумать над тем, как минимизировать количество переключений контекста при исполнении Вашего кода. *Я не буду давать рекомендаций по поводу создания высокопроизводительного WEB-сервера, поскольку задача эта больше подходит для специализированных языков программирования WEB-серверов, например GoLang, а Delphi заточен в первую очередь на разработку фронтэнда, а также корпоративного программного обеспечения.*
|
||||
|
||||
<!--
|
||||
Может информация будет полезной. В языке GoLang очень эффективно реализована многопоточность. За счёт этого обеспечивается очень высокая производительность.
|
||||
По сути там есть очередь задач (это очередь go-рутин, т.е. ссылок на "анонимные методы", если по аналогии с Delphi). Но это не простая очередь! worker-поток при переводе текущей go-рутины в "заблокированное" состояние, копирует в эту очередь состояние текущего стека и регистров. Worker-потоки, обслуживающие, go-рутины, очень редко переходят в режим сна (только если нет высокой нагрузки). После перевода текущей go-рутины в "заблокированное" состояние worker-поток отыскивает в очереди go-рутин следующу go-рутину, готовую к исполнению, загружает её стек и состояние регистров и выполнение новой go-рутины продолжается как ни в чём не бывало. Тяжёлый механизм переключения контекста потоков (как в ОС) не используется.
|
||||
Фактически, если нагрузка высокая, то worker-потоки успевают за один квант времени, выделенный ОС (без переключения контекста потока), обработать сотни go-рутин.
|
||||
|
||||
Функции ОС, которые могут заблокировать worker-поток, в GoLang не используются. Например, вместо виндового Sleep используется свой встроенный Sleep, который блокирует текущую go-рутину на заданный промежуток времени, а worker-поток затем загружает из очереди очередную go-рутину, готовую к исполнению.
|
||||
|
||||
Скорее всего в GoLang есть какой-то отдельный поток, который периодически проверяет очередь go-рутин и как-то их сортирует, чтобы worker-потоку каждый раз не проверять всю очередь с начала до конца.
|
||||
|
||||
Количество worker-потоков в GoLang обычно соответствует числу ядер процессора. Обычно нет смысла делать больше потоков, только если приходится обращаться к внешнему API, которое может заблокировать worker-поток.
|
||||
-->
|
||||
|
||||
# 2. Базовый класс многопоточности - TThread <a name="class_tthread"></a>
|
||||
|
||||
`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 Критическая секция и монитор <a name="sync_crit_sect"></a>
|
||||
|
||||
@ -2521,6 +2532,57 @@ end;
|
||||
|
||||
:warning: **Внимание!** Следует быть осторожным при объявлении в секции `threadvar` некоторых типов (строки, динамические массивы, варианты, интерфейсы). Причины этого хорошо описаны в [официальной документации](http://docwiki.embarcadero.com/RADStudio/Rio/en/Variables_(Delphi)#Thread-local_Variables).
|
||||
|
||||
# 12. Работа с базой данных из дополнительного потока <a name="work_with_db"></a>
|
||||
|
||||
Лично я не рекомендую работать с базой данных из десктопного приложения напрямую. Намного перспективнее разработать, как сейчас модно говорить, "микросервис", который:
|
||||
а) будет находиться на том же компьютере, что и база данных (либо на соседнем сервере);
|
||||
б) сможет работать с базой данных по наиболее быстрому протоколу с минимальными сетевыми задержками;
|
||||
в) сможет принимать от клиентов с помощью 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. Уничтожаем объект подключения либо возвращаем его обратно в пул.
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
:warning: **Внимание!**
|
||||
:exclamation: **Внимание!**
|
||||
|
Loading…
x
Reference in New Issue
Block a user