Comments 65
-18
А чем вас не устроил стандартный TThread?
0
Это вы мне?
-3
TThread никуда не девается. TThread — это обертка над базовыми WinAPI функциями. Более того, на нижнем уровне библиотека OTL класс TThread успешно используется. Здесь же речь идет об уровнях гораздо более высоких по сравнению с TThread. Вы попробуйте сначала реализовать на основе голого TThread, например, конвейер (Pipeline) и тогда поймете, в чем разница.
+1
Извините, промахнулся. Вопрос адресован ТС
0
Ну например когда я еще увлекался программизмом — тоже обожал Дельфи. Красивый язык, с которым не приходится изобретать велосипеды, но надо под каждый чих качать компоненты. Его идеологию ИМХО перенял C#.
+1
Красивый язык, с которым не приходится изобретать велосипеды
Был в свое время, версии эдак до 6-й.
Его идеологию ИМХО перенял C#.
Внезапно, потому что их проектировал один и тот же человек. Собственно, примерно с момента как Хейльсберг ушел в Майкрософт, Дельфи и покатился по наклонной.
Был в свое время, версии эдак до 6-й.
Его идеологию ИМХО перенял C#.
Внезапно, потому что их проектировал один и тот же человек. Собственно, примерно с момента как Хейльсберг ушел в Майкрософт, Дельфи и покатился по наклонной.
-3
Его уникальность в том, что он одновременно позволяет создавать код сколь угодно высокого уровня, при этом оставаясь «близким к железу», т.к. на выходе мы получаем native-приложение, а не код для виртуальной машины Java или .Net.
Ни в Java, ни в .Net нет никаких виртуальных машин. Там есть JIT-компиляторы, которые работают не медленнее дельфовского RTTL.
Ни в Java, ни в .Net нет никаких виртуальных машин. Там есть JIT-компиляторы, которые работают не медленнее дельфовского RTTL.
-14
* RTTI, конечно.
-7
Управляемое окружение.
0
коды на языке 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
+3
blogs.msdn.com/b/brada/archive/2005/01/12/351958.aspx
Давайте сойдемся на том, что вопрос спорный и касается скорее понимания терминологии.
В Java когда-то была действительно полноценная виртуальной машина.
Давайте сойдемся на том, что вопрос спорный и касается скорее понимания терминологии.
В Java когда-то была действительно полноценная виртуальной машина.
-3
Надо было в статье еще об AsyncCalls упомянуть.
+2
Есть еще JEDI Core и JEDI VCL и там набор всевозможных компонентов JvThread, JvThreadTimer и т.д.
0
Еще раз акцентирую внимание читателей (видимо в статье не получилось поставить этот акцент), OmniThreadLibrary предоставляет гораздо более высокоуровневый подход к написанию многопоточного кода по сравнению с WinAPI и обертками типа TThread. Из приведенных выше примеров (какими бы примитивными они ни были) видно, что ни методов создания/уничтожения потоков, ни функций ожидания/синхронизации явным образом вызывать не нужно, все спрятано за очень красивыми и удобными абстракциями.
0
>>видно, что ни методов создания/уничтожения потоков, ни функций ожидания/синхронизации явным образом вызывать не нужно, все спрятано за очень красивыми и удобными абстракциями.
А есть примеры более сложных программ с использованием OmniThreadLibrary?
Интересует примеры:
1. Работа нескольких потоков с одним массивом данных.
2. Доступ и работа с БД из нескольких потоков.
3. Работа нескольких потоков по расчету каких-либо данных и вывод информации на форму, к примеру построение графиков.
А есть примеры более сложных программ с использованием OmniThreadLibrary?
Интересует примеры:
1. Работа нескольких потоков с одним массивом данных.
2. Доступ и работа с БД из нескольких потоков.
3. Работа нескольких потоков по расчету каких-либо данных и вывод информации на форму, к примеру построение графиков.
0
>>1.Работа нескольких потоков с одним массивом данных.
Слишком общее описание вопроса, непонятно, какую именно задачу вы решаете.
>>2.Доступ и работа с БД из нескольких потоков.
Здесь все очень тонко. Если у вас один Connection, то вы сможете использовать его только в одном потоке. Если создавать Connection для каждого потока, который может обращаться к базе, то появляется риск нарваться на блокировки с самим собой не уровне СУБД.
>>3.
Опять же слишком общее описание.
OTL снабжена значительным количеством примеров, позволяющих быстро разобраться с основными идеями и начать применять библиотеку на практике.
Слишком общее описание вопроса, непонятно, какую именно задачу вы решаете.
>>2.Доступ и работа с БД из нескольких потоков.
Здесь все очень тонко. Если у вас один Connection, то вы сможете использовать его только в одном потоке. Если создавать Connection для каждого потока, который может обращаться к базе, то появляется риск нарваться на блокировки с самим собой не уровне СУБД.
>>3.
Опять же слишком общее описание.
OTL снабжена значительным количеством примеров, позволяющих быстро разобраться с основными идеями и начать применять библиотеку на практике.
0
>>OTL снабжена значительным количеством примеров, позволяющих быстро разобраться с основными идеями и начать применять библиотеку на практике.
В папке examples всего один пример: stringlist parser :(
Есть конечно много чего в папке tests, Вы про это говорите?
В папке examples всего один пример: stringlist parser :(
Есть конечно много чего в папке tests, Вы про это говорите?
0
>И при этом язык Delphi очень прост и лаконичен, код на нем приятно читать и в нем достаточно легко разобраться, чего не могу сказать о коде на C или C++
Вот не скажи, одни только begin и end как код раздувают по размеру, да и синтаксис объявления функций весьма громоздкий. Для обучения хорошо, а тут только строчки лишние плодятся.
Ну и в плюсах тоже есть std::future в аналогичным функционалом. Да и в Qt уже столет в обед она была.
Вот не скажи, одни только begin и end как код раздувают по размеру, да и синтаксис объявления функций весьма громоздкий. Для обучения хорошо, а тут только строчки лишние плодятся.
Ну и в плюсах тоже есть std::future в аналогичным функционалом. Да и в Qt уже столет в обед она была.
-5
Вот не скажи, одни только begin и end как код раздувают по размеру, да и синтаксис объявления функций весьма громоздкий.
Это никак не противоречит «код на нем приятно читать и в нем достаточно легко разобраться».
+3
FYI. Мне кажется, что это — лучшая статья про потоки в Delphi. Думаю, что перед тем как пользоваться оболочками в виде OmniThreadLibrary, надо понимать основы.
0
Ага, читал. Отличная статья на момент ее написания. Когда попробовал OTL, понял что многими низкоуровневыми вещами для написания «первого многопоточного приложения» можно голову не забивать. OTL позволяет концентрироваться на полезном коде приложения, а не коде для поддержки запуска, остановки, уничтожения, синхронизации потоков.
0
Для первого и простого можно. Но если я напишу что-нибудь сложное с OTL, и в процессе тестирования вылезут гейзенбаги, то я бы хотел знать основы, чтобы хотя бы предположить, что пошло не так. Многопоточность не настолько простая область, имхо, чтобы совсем про нее не читать и довериться сторонним библиотекам.
0
Большое спасибо за статью!
Заинтересовало…
Присоединяюсь к просьбе рассмотреть пример: доступ и работа с БД из нескольких потоков.
А именно
1) с одной сессией
2) с несколькими сессиями.
Ваши рекомендации по этому поводу?
Заранее спасибо.
Заинтересовало…
Присоединяюсь к просьбе рассмотреть пример: доступ и работа с БД из нескольких потоков.
А именно
1) с одной сессией
2) с несколькими сессиями.
Ваши рекомендации по этому поводу?
Заранее спасибо.
0
Извиняюсь, а в чем проблема работать с БД из нескольких потоков? Откройте несколько соединений к БД (т.е. каждый поток использует свое соединение) и работайте.
«Сессия» — имеется в виду прикладной объект с данными? Защитите его примитивным объектом синхронизации.
«Сессия» — имеется в виду прикладной объект с данными? Защитите его примитивным объектом синхронизации.
0
Если под сессией вы понимаете соединение (Connection), то с одним соединением из нескольких потоков работать не получится (разве что по очереди, что никакого смысла в рассматриваемом контексте не имеет), а с несколькими соединениями — технических ограничений нет. Но если у вас 2-х звенка и вы используете data-aware controls, то все попытки прикрутить сюда многопоточность — это в любом случае будет кривой костыль. Если у вас 3-х звенка, то тут уже могут быть варианты (зависит от архитектуры сервера приложений). Правда ИМХО при доступе к БД использовать многопоточность можно только для read-only запросов (SELECT) и только если согласованность данных, получаемых несколькими запросами, для вас значения не имеет, т.к. во всех остальных случаях (когда вам нужно из нескольких потоков изменять данные в БД или если согласованность данных, выбираемых несколькими запросами, для вас важна) использование многопоточности приведет к нежелательным и сложно отлавливаемым ошибкам и только запутает код.
Многопоточность при работе с БД удобно применять, когда какой-то процесс можно разделить на действия, не связанные с доступом к БД (например, чтение какой-то информации из файлов), и уже собственно на доступ к БД (запись информации из файлов в БД). Эти части можно выполнять независимо (например, используя многопоточный конвейер).
Многопоточность при работе с БД удобно применять, когда какой-то процесс можно разделить на действия, не связанные с доступом к БД (например, чтение какой-то информации из файлов), и уже собственно на доступ к БД (запись информации из файлов в БД). Эти части можно выполнять независимо (например, используя многопоточный конвейер).
0
>Но если у вас 2-х звенка и вы используете data-aware controls, то все попытки >прикрутить сюда многопоточность — это в любом случае будет кривой костыль.
Я знал… я так и знал… :-)
>Эти части можно выполнять независимо (например, используя многопоточный конвейер)
А это как, простите?..
Я знал… я так и знал… :-)
>Эти части можно выполнять независимо (например, используя многопоточный конвейер)
А это как, простите?..
0
Имел в виду Pipeline из OmniThreadLibrary. Один этап конвейера читает файлы по одному, берет из них необходимую информацию и передает второму этапу. Второй этап выполняет запросы к БД. Этапы выполняются в параллельных потоках и никак не мешают друг другу, а ускорение налицо (чтение файлов происходит параллельно с выполнением запросов по обновлению данных в БД).
Признаться, рассказывать об основах многопоточной разработки в комментариях к статье довольно сложно :)
Признаться, рассказывать об основах многопоточной разработки в комментариях к статье довольно сложно :)
0
Простите, а что означает в строчке
vFuture: IOmniFuture;
тип integer в угловых скобках? :-) Или как это называется, и где про это почитать? :-)
vFuture: IOmniFuture;
тип integer в угловых скобках? :-) Или как это называется, и где про это почитать? :-)
0
Похоже на адаптацию .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. А это могло бы существенно упростить жизнь.
0
Вообще то стандартный метод синхронизации потоков с GUI это Synchronize:
docwiki.embarcadero.com/Libraries/en/System.Classes.TThread.Synchronize
docwiki.embarcadero.com/Libraries/en/System.Classes.TThread.Synchronize
0
>>Похоже на адаптацию .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);
0
Забыл пометить код как дельфийский. Пишу еще раз:
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);
0
Ну с Invoke конечно веселее. Да, я от новостей о Delphi отстал, когда я в последний раз на ней писал, там еще не было анонимнах методов, обобщений и поддержки Unicode.
0
Чем плох Synchronize я там так и не увидел. И чем это лучше Synchronize тоже не понятно.
0
Мой RequestDialog ничем не лучше Synchonize, это как раз эмуляция Synchronize средствами OTL. А вот исходный метод Invoke лучше чем Synchronize тем, что позволяет вызвать из любого потока метод в контексте другого потока при этом не блокируя вызывающий поток. Вызов лишь «ставится в очередь» и вызывающий поток продолжает свою работу. Поток-адоресат выполнит метод в тот момент, когда прочитает из своей очереди сообщений соответствующее сообщение.
0
Насколько помню, в VCL все в порядке с message loop, просто нужно немного понимать, как он работает.
Тут уже упомянули про Synchronize() — это банально выполнение метода (процедуры) в основном потоке, перед переходом в idle. Дешево и сердито, но глючный поток может поломать всю программу.
Правильнее было бы создание простейшей глобальной thread-safe очереди сообщений на базе TThreadList, и обрабатывать ее по таймеру. Тогда код дочерних потоков будет полностью изолирован от кода основного потока, без лишних блокировок и заморочек. Даже если поток сломается или зависнет — прога просто поймает исключение в TApplicationEvents.OnException() и продолжит полет.
Тут уже упомянули про Synchronize() — это банально выполнение метода (процедуры) в основном потоке, перед переходом в idle. Дешево и сердито, но глючный поток может поломать всю программу.
Правильнее было бы создание простейшей глобальной thread-safe очереди сообщений на базе TThreadList, и обрабатывать ее по таймеру. Тогда код дочерних потоков будет полностью изолирован от кода основного потока, без лишних блокировок и заморочек. Даже если поток сломается или зависнет — прога просто поймает исключение в TApplicationEvents.OnException() и продолжит полет.
0
Да, в VCL c message loop действительно все в порядке. Имелась в виду ситуация
Обработку сообщений по таймеру и «правильнее было бы» даже комментировать не буду.
procedure A;
begin
// Запускаем доп. поток
// ... Что-то делаем ... В это время второй поток хочет сделать Synchronyze
// Ждем завершения второго потока. Вот здесь надо делать ProcessMessages вручную, иначе можно ждать бесконечно
end;
Обработку сообщений по таймеру и «правильнее было бы» даже комментировать не буду.
0
Нежно любил Дельфю-7 за простой и читабельный синтаксис, удобные библиотеки и компоненты.
В новых версиях синтаксис испортился, при этом мало что улучшилось. Ну и хрен с ними.
А теперь открыл для себя Python, чего и вам желаю!
В новых версиях синтаксис испортился, при этом мало что улучшилось. Ну и хрен с ними.
А теперь открыл для себя Python, чего и вам желаю!
0
>В новых версиях синтаксис испортился
Это чем же он испортился интересно. Дженериками и анонимными методами? ;)
Это чем же он испортился интересно. Дженериками и анонимными методами? ;)
0
Оними самыми. Хотели как лучше, а получилось как у всех.
0
Не вижу причин для недовольства. Отличный функционал, ИМХО.
0
TObjectList вместо TObjectList это уже прорыв (кроме шуток)
0
На мой взгляд это уродливый костыль для обхода ограничений типизации.
0
Это не обход ограничений типизации, а наоборот, усиление типизации. Обычный TObjectList может хранить объекты любого (абсолютно любого) класса-наследника TObject. А «TObjectList угловая_скобка TPerson угловая_скобка» может хранить только объекты класса TPerson и его наследников, что позволяет не делать каждый раз преобразование «Items[i] as TPerson», а сразу обращаться к Items[i] как к TPerson.
0
А могло быть и так:
Без всяких этих извращений. Один хрен ведь типы заранее известны и в коде, и в 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), нафиг такие сложности с принудительной типизацией.
В свое время Борланд добавил кучу послаблений (отключаемых) для паскаля, и было всем удобно. Только компилятору было неудобно, но его мнения никто не спрашивал. А гадкий кодежир решил сделать удобно компилятору. Вот и пусть компилятор сам программы пишет.
0
Некоторый Duck typing таки народился в 10.3 Rio — см. docwiki.embarcadero.com/RADStudio/Rio/en/Inline_Variable_Declaration#Type_Inference_for_Inlined_Variables
Но, кажись, не на таком ещё уровне…
Но, кажись, не на таком ещё уровне…
0
За эти 6 лет мое мнение о типизации и Python несколько поменялось. Типизация нужна, и чем строже, тем лучше. Но безопасный способ ее обойти (без жесткого typecast) тоже бывает нужен. Что-то вроде variant с поддержкой классов, когда можно попытаться вызвать какой-то метод наугад, и это приведет не к segfault, а к цивильному исключению несовпадения типа.
Дженерики и анонимные методы я так ни разу и не использовал в Delphi. В Java и Kotlin так и не привык к анонимным процедурам, очень портят структуру кода.
Дженерики и анонимные методы я так ни разу и не использовал в Delphi. В Java и Kotlin так и не привык к анонимным процедурам, очень портят структуру кода.
+1
Это изменение мнения по строгость типизации могу только приветствовать. Очень рад.
IDispatch для вариантов в Delphi давно поддерживается, но он сделан (и, кажется, до сих пор) без Code Insight, нормальной документации и без нормальных средств анализа и отладки, так что работать с ним очень болезненно ((
А на счёт дженериков и анонимных процедур — ИМХО, вопрос поддержки IDE, отладчиком и документацией тут тоже критичен. Но штуки вкусные, ИМХО.
IDispatch для вариантов в Delphi давно поддерживается, но он сделан (и, кажется, до сих пор) без Code Insight, нормальной документации и без нормальных средств анализа и отладки, так что работать с ним очень болезненно ((
А на счёт дженериков и анонимных процедур — ИМХО, вопрос поддержки IDE, отладчиком и документацией тут тоже критичен. Но штуки вкусные, ИМХО.
0
Имел в виду «TObjectList угловая скобка T угловая скобка» вместо TObjectList (хабр вырезает их)
0
Sign up to leave a comment.
Библиотека OmniThreadLibrary — простая многопоточность в среде Delphi