Комментарии 65
А чем вас не устроил стандартный TThread?
Это вы мне?
TThread никуда не девается. TThread — это обертка над базовыми WinAPI функциями. Более того, на нижнем уровне библиотека OTL класс TThread успешно используется. Здесь же речь идет об уровнях гораздо более высоких по сравнению с TThread. Вы попробуйте сначала реализовать на основе голого TThread, например, конвейер (Pipeline) и тогда поймете, в чем разница.
Извините, промахнулся. Вопрос адресован ТС
Ну например когда я еще увлекался программизмом — тоже обожал Дельфи. Красивый язык, с которым не приходится изобретать велосипеды, но надо под каждый чих качать компоненты. Его идеологию ИМХО перенял C#.
Красивый язык, с которым не приходится изобретать велосипеды
Был в свое время, версии эдак до 6-й.
Его идеологию ИМХО перенял C#.
Внезапно, потому что их проектировал один и тот же человек. Собственно, примерно с момента как Хейльсберг ушел в Майкрософт, Дельфи и покатился по наклонной.
Был в свое время, версии эдак до 6-й.
Его идеологию ИМХО перенял C#.
Внезапно, потому что их проектировал один и тот же человек. Собственно, примерно с момента как Хейльсберг ушел в Майкрософт, Дельфи и покатился по наклонной.
Его уникальность в том, что он одновременно позволяет создавать код сколь угодно высокого уровня, при этом оставаясь «близким к железу», т.к. на выходе мы получаем native-приложение, а не код для виртуальной машины Java или .Net.
Ни в Java, ни в .Net нет никаких виртуальных машин. Там есть JIT-компиляторы, которые работают не медленнее дельфовского RTTL.
Ни в Java, ни в .Net нет никаких виртуальных машин. Там есть JIT-компиляторы, которые работают не медленнее дельфовского RTTL.
* RTTI, конечно.
Управляемое окружение.
коды на языке C#, например, компилируются в промежуточный байт-код (MSIL).
далее VM (CLR в данном случае) вызывает JIT-компилятор для компиляции MSIL в машинные инструкции.
CLR обеспечивает управление потоками, GC, JIT, Обработку исключений и еще много чего.
>>Ни в Java, ни в .Net нет никаких виртуальных машин.
пожалуйста, объясните это разработчикам Java Virtual Machine (JVM), а то они неправильно используют термины.
не надо путать VM для виртуализации и Process VM
далее VM (CLR в данном случае) вызывает JIT-компилятор для компиляции MSIL в машинные инструкции.
CLR обеспечивает управление потоками, GC, JIT, Обработку исключений и еще много чего.
>>Ни в Java, ни в .Net нет никаких виртуальных машин.
пожалуйста, объясните это разработчикам Java Virtual Machine (JVM), а то они неправильно используют термины.
не надо путать VM для виртуализации и Process VM
blogs.msdn.com/b/brada/archive/2005/01/12/351958.aspx
Давайте сойдемся на том, что вопрос спорный и касается скорее понимания терминологии.
В Java когда-то была действительно полноценная виртуальной машина.
Давайте сойдемся на том, что вопрос спорный и касается скорее понимания терминологии.
В Java когда-то была действительно полноценная виртуальной машина.
Надо было в статье еще об AsyncCalls упомянуть.
Есть еще JEDI Core и JEDI VCL и там набор всевозможных компонентов JvThread, JvThreadTimer и т.д.
Еще раз акцентирую внимание читателей (видимо в статье не получилось поставить этот акцент), OmniThreadLibrary предоставляет гораздо более высокоуровневый подход к написанию многопоточного кода по сравнению с WinAPI и обертками типа TThread. Из приведенных выше примеров (какими бы примитивными они ни были) видно, что ни методов создания/уничтожения потоков, ни функций ожидания/синхронизации явным образом вызывать не нужно, все спрятано за очень красивыми и удобными абстракциями.
>>видно, что ни методов создания/уничтожения потоков, ни функций ожидания/синхронизации явным образом вызывать не нужно, все спрятано за очень красивыми и удобными абстракциями.
А есть примеры более сложных программ с использованием OmniThreadLibrary?
Интересует примеры:
1. Работа нескольких потоков с одним массивом данных.
2. Доступ и работа с БД из нескольких потоков.
3. Работа нескольких потоков по расчету каких-либо данных и вывод информации на форму, к примеру построение графиков.
А есть примеры более сложных программ с использованием OmniThreadLibrary?
Интересует примеры:
1. Работа нескольких потоков с одним массивом данных.
2. Доступ и работа с БД из нескольких потоков.
3. Работа нескольких потоков по расчету каких-либо данных и вывод информации на форму, к примеру построение графиков.
>>1.Работа нескольких потоков с одним массивом данных.
Слишком общее описание вопроса, непонятно, какую именно задачу вы решаете.
>>2.Доступ и работа с БД из нескольких потоков.
Здесь все очень тонко. Если у вас один Connection, то вы сможете использовать его только в одном потоке. Если создавать Connection для каждого потока, который может обращаться к базе, то появляется риск нарваться на блокировки с самим собой не уровне СУБД.
>>3.
Опять же слишком общее описание.
OTL снабжена значительным количеством примеров, позволяющих быстро разобраться с основными идеями и начать применять библиотеку на практике.
Слишком общее описание вопроса, непонятно, какую именно задачу вы решаете.
>>2.Доступ и работа с БД из нескольких потоков.
Здесь все очень тонко. Если у вас один Connection, то вы сможете использовать его только в одном потоке. Если создавать Connection для каждого потока, который может обращаться к базе, то появляется риск нарваться на блокировки с самим собой не уровне СУБД.
>>3.
Опять же слишком общее описание.
OTL снабжена значительным количеством примеров, позволяющих быстро разобраться с основными идеями и начать применять библиотеку на практике.
>>OTL снабжена значительным количеством примеров, позволяющих быстро разобраться с основными идеями и начать применять библиотеку на практике.
В папке examples всего один пример: stringlist parser :(
Есть конечно много чего в папке tests, Вы про это говорите?
В папке examples всего один пример: stringlist parser :(
Есть конечно много чего в папке tests, Вы про это говорите?
>И при этом язык Delphi очень прост и лаконичен, код на нем приятно читать и в нем достаточно легко разобраться, чего не могу сказать о коде на C или C++
Вот не скажи, одни только begin и end как код раздувают по размеру, да и синтаксис объявления функций весьма громоздкий. Для обучения хорошо, а тут только строчки лишние плодятся.
Ну и в плюсах тоже есть std::future в аналогичным функционалом. Да и в Qt уже столет в обед она была.
Вот не скажи, одни только begin и end как код раздувают по размеру, да и синтаксис объявления функций весьма громоздкий. Для обучения хорошо, а тут только строчки лишние плодятся.
Ну и в плюсах тоже есть std::future в аналогичным функционалом. Да и в Qt уже столет в обед она была.
Вот не скажи, одни только begin и end как код раздувают по размеру, да и синтаксис объявления функций весьма громоздкий.
Это никак не противоречит «код на нем приятно читать и в нем достаточно легко разобраться».
FYI. Мне кажется, что это — лучшая статья про потоки в Delphi. Думаю, что перед тем как пользоваться оболочками в виде OmniThreadLibrary, надо понимать основы.
Ага, читал. Отличная статья на момент ее написания. Когда попробовал OTL, понял что многими низкоуровневыми вещами для написания «первого многопоточного приложения» можно голову не забивать. OTL позволяет концентрироваться на полезном коде приложения, а не коде для поддержки запуска, остановки, уничтожения, синхронизации потоков.
Для первого и простого можно. Но если я напишу что-нибудь сложное с OTL, и в процессе тестирования вылезут гейзенбаги, то я бы хотел знать основы, чтобы хотя бы предположить, что пошло не так. Многопоточность не настолько простая область, имхо, чтобы совсем про нее не читать и довериться сторонним библиотекам.
Большое спасибо за статью!
Заинтересовало…
Присоединяюсь к просьбе рассмотреть пример: доступ и работа с БД из нескольких потоков.
А именно
1) с одной сессией
2) с несколькими сессиями.
Ваши рекомендации по этому поводу?
Заранее спасибо.
Заинтересовало…
Присоединяюсь к просьбе рассмотреть пример: доступ и работа с БД из нескольких потоков.
А именно
1) с одной сессией
2) с несколькими сессиями.
Ваши рекомендации по этому поводу?
Заранее спасибо.
Извиняюсь, а в чем проблема работать с БД из нескольких потоков? Откройте несколько соединений к БД (т.е. каждый поток использует свое соединение) и работайте.
«Сессия» — имеется в виду прикладной объект с данными? Защитите его примитивным объектом синхронизации.
«Сессия» — имеется в виду прикладной объект с данными? Защитите его примитивным объектом синхронизации.
Если под сессией вы понимаете соединение (Connection), то с одним соединением из нескольких потоков работать не получится (разве что по очереди, что никакого смысла в рассматриваемом контексте не имеет), а с несколькими соединениями — технических ограничений нет. Но если у вас 2-х звенка и вы используете data-aware controls, то все попытки прикрутить сюда многопоточность — это в любом случае будет кривой костыль. Если у вас 3-х звенка, то тут уже могут быть варианты (зависит от архитектуры сервера приложений). Правда ИМХО при доступе к БД использовать многопоточность можно только для read-only запросов (SELECT) и только если согласованность данных, получаемых несколькими запросами, для вас значения не имеет, т.к. во всех остальных случаях (когда вам нужно из нескольких потоков изменять данные в БД или если согласованность данных, выбираемых несколькими запросами, для вас важна) использование многопоточности приведет к нежелательным и сложно отлавливаемым ошибкам и только запутает код.
Многопоточность при работе с БД удобно применять, когда какой-то процесс можно разделить на действия, не связанные с доступом к БД (например, чтение какой-то информации из файлов), и уже собственно на доступ к БД (запись информации из файлов в БД). Эти части можно выполнять независимо (например, используя многопоточный конвейер).
Многопоточность при работе с БД удобно применять, когда какой-то процесс можно разделить на действия, не связанные с доступом к БД (например, чтение какой-то информации из файлов), и уже собственно на доступ к БД (запись информации из файлов в БД). Эти части можно выполнять независимо (например, используя многопоточный конвейер).
>Но если у вас 2-х звенка и вы используете data-aware controls, то все попытки >прикрутить сюда многопоточность — это в любом случае будет кривой костыль.
Я знал… я так и знал… :-)
>Эти части можно выполнять независимо (например, используя многопоточный конвейер)
А это как, простите?..
Я знал… я так и знал… :-)
>Эти части можно выполнять независимо (например, используя многопоточный конвейер)
А это как, простите?..
Имел в виду Pipeline из OmniThreadLibrary. Один этап конвейера читает файлы по одному, берет из них необходимую информацию и передает второму этапу. Второй этап выполняет запросы к БД. Этапы выполняются в параллельных потоках и никак не мешают друг другу, а ускорение налицо (чтение файлов происходит параллельно с выполнением запросов по обновлению данных в БД).
Признаться, рассказывать об основах многопоточной разработки в комментариях к статье довольно сложно :)
Признаться, рассказывать об основах многопоточной разработки в комментариях к статье довольно сложно :)
Простите, а что означает в строчке
vFuture: IOmniFuture;
тип integer в угловых скобках? :-) Или как это называется, и где про это почитать? :-)
vFuture: IOmniFuture;
тип integer в угловых скобках? :-) Или как это называется, и где про это почитать? :-)
Похоже на адаптацию .NET'ного Thread Pool'а и механизма асинхронного вызова делегатов Delegate.BeginInvoke, работающего на этом пуле, а так же от части WPF'ного Dispatcher'а. В C# 4.5 будет готовый синтаксический сахар для всех этих дел — ключевые слова async и wait.
Что касается чисто Delphi, многопоточность там приемлема, пока дело не доходит до взаимодействия с GUI. На моей практике в подавляющем большинстве случаев задача заключалась в необходимости запустить асинхронную операцию с выводом процента выполнения в GUI. А до GUI из фоновых потоков можно добраться только через одно место — очередь оконных сообщений основного потока (SendMessage, PostMessage), и через набор обработчиков этих сообщений с другой стороны. А всё по тому, что кое-кто не озаботился реализацией alertable message loop'а в VCL, что не позволяет производить «инъекции» своих методов обратного вызова в поток, обрабатывающий оконные сообщения через APC. А это могло бы существенно упростить жизнь.
Что касается чисто Delphi, многопоточность там приемлема, пока дело не доходит до взаимодействия с GUI. На моей практике в подавляющем большинстве случаев задача заключалась в необходимости запустить асинхронную операцию с выводом процента выполнения в GUI. А до GUI из фоновых потоков можно добраться только через одно место — очередь оконных сообщений основного потока (SendMessage, PostMessage), и через набор обработчиков этих сообщений с другой стороны. А всё по тому, что кое-кто не озаботился реализацией alertable message loop'а в VCL, что не позволяет производить «инъекции» своих методов обратного вызова в поток, обрабатывающий оконные сообщения через APC. А это могло бы существенно упростить жизнь.
Вообще то стандартный метод синхронизации потоков с GUI это Synchronize:
docwiki.embarcadero.com/Libraries/en/System.Classes.TThread.Synchronize
docwiki.embarcadero.com/Libraries/en/System.Classes.TThread.Synchronize
>>Похоже на адаптацию .NET'ного Thread Pool'а
Это не адаптация. Это другая реализация похожих вещей (с нуля).
>>что не позволяет производить «инъекции» своих методов обратного вызова в поток
С помощью OTL такую инъекцию как раз можно произвести ;). Причем не только в главный поток, но и в любой другой. Если инъекция делается в главный поток, то он естественно должен находиться в активной петле обработки сообщений. Вот тут как раз об этом написано и также поясняется чем плох Synchronize. Мне удалось «эмулировать» Synchronize и при использовании неблокирующего Invoke, позволив таким образом вызывать пользовательские диалоги из дополнительных потоков (ну не красота ли?). Для этого пришлось написать такой метод:
// Глобальная крит. секция на на взаимодействие с пользователем (чтобы один диалог, сделав Application.ProcessMessages, не мог вызвать «из-под себя» другой диалог (инициированный другим потоком, но еще не активированный)
var
lParallelDialogCS: TCriticalSection;
procedure MainThreadOnly;
begin
Assert(GetCurrentThreadID = MainThreadID, 'Данный участок программы может выполняться только в основном потоке.');
end;
procedure RequestDialog(const aTask: IOmniTask;
aDialogProc: TOmniTaskInvokeFunction);
var
vCompleted: IOmniWaitableValue;
vExc: Exception;
begin
if not Assigned(aTask) then
begin
MainThreadOnly;
aDialogProc;
end
else begin
lParallelDialogCS.Enter;
try
vExc := nil;
vCompleted := CreateWaitableValue;
aTask.Invoke(
procedure
begin
try
aDialogProc;
except
vExc := AcquireExceptionObject;
end;
vCompleted.Signal;
end
);
vCompleted.WaitFor;
if Assigned(vExc) then
raise vExc;
finally
lParallelDialogCS.Leave;
end;
end;
end;
// Инициализация крит. секции осуществляется самим unit'ом
initialization
lParallelDialogCS := TCriticalSection.Create;
finalization
FreeAndNil(lParallelDialogCS);
Это не адаптация. Это другая реализация похожих вещей (с нуля).
>>что не позволяет производить «инъекции» своих методов обратного вызова в поток
С помощью OTL такую инъекцию как раз можно произвести ;). Причем не только в главный поток, но и в любой другой. Если инъекция делается в главный поток, то он естественно должен находиться в активной петле обработки сообщений. Вот тут как раз об этом написано и также поясняется чем плох Synchronize. Мне удалось «эмулировать» Synchronize и при использовании неблокирующего Invoke, позволив таким образом вызывать пользовательские диалоги из дополнительных потоков (ну не красота ли?). Для этого пришлось написать такой метод:
// Глобальная крит. секция на на взаимодействие с пользователем (чтобы один диалог, сделав Application.ProcessMessages, не мог вызвать «из-под себя» другой диалог (инициированный другим потоком, но еще не активированный)
var
lParallelDialogCS: TCriticalSection;
procedure MainThreadOnly;
begin
Assert(GetCurrentThreadID = MainThreadID, 'Данный участок программы может выполняться только в основном потоке.');
end;
procedure RequestDialog(const aTask: IOmniTask;
aDialogProc: TOmniTaskInvokeFunction);
var
vCompleted: IOmniWaitableValue;
vExc: Exception;
begin
if not Assigned(aTask) then
begin
MainThreadOnly;
aDialogProc;
end
else begin
lParallelDialogCS.Enter;
try
vExc := nil;
vCompleted := CreateWaitableValue;
aTask.Invoke(
procedure
begin
try
aDialogProc;
except
vExc := AcquireExceptionObject;
end;
vCompleted.Signal;
end
);
vCompleted.WaitFor;
if Assigned(vExc) then
raise vExc;
finally
lParallelDialogCS.Leave;
end;
end;
end;
// Инициализация крит. секции осуществляется самим unit'ом
initialization
lParallelDialogCS := TCriticalSection.Create;
finalization
FreeAndNil(lParallelDialogCS);
Забыл пометить код как дельфийский. Пишу еще раз:
var
lParallelDialogCS: TCriticalSection; // Критическая секция на взаимодействие с пользователем из параллельного потока
procedure MainThreadOnly;
begin
Assert(GetCurrentThreadID = MainThreadID, 'Данный участок программы может выполняться только в основном потоке.');
end;
procedure RequestDialog(const aTask: IOmniTask;
aDialogProc: TOmniTaskInvokeFunction);
var
vCompleted: IOmniWaitableValue;
vExc: Exception;
begin
if not Assigned(aTask) then
begin
MainThreadOnly;
aDialogProc;
end
else begin
lParallelDialogCS.Enter;
try
vExc := nil;
vCompleted := CreateWaitableValue;
aTask.Invoke(
procedure
begin
try
aDialogProc;
except
vExc := AcquireExceptionObject;
end;
vCompleted.Signal;
end
);
vCompleted.WaitFor;
if Assigned(vExc) then
raise vExc;
finally
lParallelDialogCS.Leave;
end;
end;
end;
initialization
lParallelDialogCS := TCriticalSection.Create;
finalization
FreeAndNil(lParallelDialogCS);
Ну с Invoke конечно веселее. Да, я от новостей о Delphi отстал, когда я в последний раз на ней писал, там еще не было анонимнах методов, обобщений и поддержки Unicode.
Чем плох Synchronize я там так и не увидел. И чем это лучше Synchronize тоже не понятно.
Мой RequestDialog ничем не лучше Synchonize, это как раз эмуляция Synchronize средствами OTL. А вот исходный метод Invoke лучше чем Synchronize тем, что позволяет вызвать из любого потока метод в контексте другого потока при этом не блокируя вызывающий поток. Вызов лишь «ставится в очередь» и вызывающий поток продолжает свою работу. Поток-адоресат выполнит метод в тот момент, когда прочитает из своей очереди сообщений соответствующее сообщение.
Насколько помню, в VCL все в порядке с message loop, просто нужно немного понимать, как он работает.
Тут уже упомянули про Synchronize() — это банально выполнение метода (процедуры) в основном потоке, перед переходом в idle. Дешево и сердито, но глючный поток может поломать всю программу.
Правильнее было бы создание простейшей глобальной thread-safe очереди сообщений на базе TThreadList, и обрабатывать ее по таймеру. Тогда код дочерних потоков будет полностью изолирован от кода основного потока, без лишних блокировок и заморочек. Даже если поток сломается или зависнет — прога просто поймает исключение в TApplicationEvents.OnException() и продолжит полет.
Тут уже упомянули про Synchronize() — это банально выполнение метода (процедуры) в основном потоке, перед переходом в idle. Дешево и сердито, но глючный поток может поломать всю программу.
Правильнее было бы создание простейшей глобальной thread-safe очереди сообщений на базе TThreadList, и обрабатывать ее по таймеру. Тогда код дочерних потоков будет полностью изолирован от кода основного потока, без лишних блокировок и заморочек. Даже если поток сломается или зависнет — прога просто поймает исключение в TApplicationEvents.OnException() и продолжит полет.
Да, в VCL c message loop действительно все в порядке. Имелась в виду ситуация
Обработку сообщений по таймеру и «правильнее было бы» даже комментировать не буду.
procedure A;
begin
// Запускаем доп. поток
// ... Что-то делаем ... В это время второй поток хочет сделать Synchronyze
// Ждем завершения второго потока. Вот здесь надо делать ProcessMessages вручную, иначе можно ждать бесконечно
end;
Обработку сообщений по таймеру и «правильнее было бы» даже комментировать не буду.
Нежно любил Дельфю-7 за простой и читабельный синтаксис, удобные библиотеки и компоненты.
В новых версиях синтаксис испортился, при этом мало что улучшилось. Ну и хрен с ними.
А теперь открыл для себя Python, чего и вам желаю!
В новых версиях синтаксис испортился, при этом мало что улучшилось. Ну и хрен с ними.
А теперь открыл для себя Python, чего и вам желаю!
>В новых версиях синтаксис испортился
Это чем же он испортился интересно. Дженериками и анонимными методами? ;)
Это чем же он испортился интересно. Дженериками и анонимными методами? ;)
Оними самыми. Хотели как лучше, а получилось как у всех.
Не вижу причин для недовольства. Отличный функционал, ИМХО.
TObjectList вместо TObjectList это уже прорыв (кроме шуток)
На мой взгляд это уродливый костыль для обхода ограничений типизации.
Это не обход ограничений типизации, а наоборот, усиление типизации. Обычный TObjectList может хранить объекты любого (абсолютно любого) класса-наследника TObject. А «TObjectList угловая_скобка TPerson угловая_скобка» может хранить только объекты класса TPerson и его наследников, что позволяет не делать каждый раз преобразование «Items[i] as TPerson», а сразу обращаться к Items[i] как к TPerson.
А могло быть и так:
Без всяких этих извращений. Один хрен ведь типы заранее известны и в коде, и в runtime (RTTI), нафиг такие сложности с принудительной типизацией.
В свое время Борланд добавил кучу послаблений (отключаемых) для паскаля, и было всем удобно. Только компилятору было неудобно, но его мнения никто не спрашивал. А гадкий кодежир решил сделать удобно компилятору. Вот и пусть компилятор сам программы пишет.
function total_age(x): integer;
var i: integer;
begin
result=0;
try
for i=0 to x.count do result:=result+x[i].age;
except
print('че-то вы мне не то подсунули..');
end;
end;
Без всяких этих извращений. Один хрен ведь типы заранее известны и в коде, и в runtime (RTTI), нафиг такие сложности с принудительной типизацией.
В свое время Борланд добавил кучу послаблений (отключаемых) для паскаля, и было всем удобно. Только компилятору было неудобно, но его мнения никто не спрашивал. А гадкий кодежир решил сделать удобно компилятору. Вот и пусть компилятор сам программы пишет.
Некоторый Duck typing таки народился в 10.3 Rio — см. docwiki.embarcadero.com/RADStudio/Rio/en/Inline_Variable_Declaration#Type_Inference_for_Inlined_Variables
Но, кажись, не на таком ещё уровне…
Но, кажись, не на таком ещё уровне…
За эти 6 лет мое мнение о типизации и Python несколько поменялось. Типизация нужна, и чем строже, тем лучше. Но безопасный способ ее обойти (без жесткого typecast) тоже бывает нужен. Что-то вроде variant с поддержкой классов, когда можно попытаться вызвать какой-то метод наугад, и это приведет не к segfault, а к цивильному исключению несовпадения типа.
Дженерики и анонимные методы я так ни разу и не использовал в Delphi. В Java и Kotlin так и не привык к анонимным процедурам, очень портят структуру кода.
Дженерики и анонимные методы я так ни разу и не использовал в Delphi. В Java и Kotlin так и не привык к анонимным процедурам, очень портят структуру кода.
Это изменение мнения по строгость типизации могу только приветствовать. Очень рад.
IDispatch для вариантов в Delphi давно поддерживается, но он сделан (и, кажется, до сих пор) без Code Insight, нормальной документации и без нормальных средств анализа и отладки, так что работать с ним очень болезненно ((
А на счёт дженериков и анонимных процедур — ИМХО, вопрос поддержки IDE, отладчиком и документацией тут тоже критичен. Но штуки вкусные, ИМХО.
IDispatch для вариантов в Delphi давно поддерживается, но он сделан (и, кажется, до сих пор) без Code Insight, нормальной документации и без нормальных средств анализа и отладки, так что работать с ним очень болезненно ((
А на счёт дженериков и анонимных процедур — ИМХО, вопрос поддержки IDE, отладчиком и документацией тут тоже критичен. Но штуки вкусные, ИМХО.
Имел в виду «TObjectList угловая скобка T угловая скобка» вместо TObjectList (хабр вырезает их)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Библиотека OmniThreadLibrary — простая многопоточность в среде Delphi