Пользователь
0,1
рейтинг
28 мая 2012 в 11:48

Разработка → Библиотека OmniThreadLibrary — простая многопоточность в среде Delphi

Написать интересную статью на техническую тему очень сложно. Приходится балансировать между тем, чтобы не скатиться в технические дебри и тем, чтобы совсем ничего не сказать. Сегодня я попробую в общих словах (без деталей) поговорить о том, как обстоят дела с разработкой многопоточных desktop-приложений в не столь популярной на сегодняшний день, но наверняка знакомой многим российским разработчикам среде Delphi. Статья ориентирована на НЕ новичков в программировании, являющихся при этом новичками в области создания многопоточных приложений.


Затронутая в заголовке тема очень обширна. Все, что будет написано ниже, — это даже не верхушка айсберга, это скорее полет на высоте 10000 метров над океаном, в котором эти айсберги плавают. Зачем писать такую статью? Скорее для того, чтобы обратить внимание на широкие возможности, которые уже давно доступны, но которых почему-то многие побаиваются и сторонятся.

Почему Delphi?


Я программирую на Delphi очень давно и не перестаю наслаждаться. Это во многих отношениях замечательный язык. Его уникальность в том, что он одновременно позволяет создавать код сколь угодно высокого уровня, при этом оставаясь «близким к железу», т.к. на выходе мы получаем native-приложение, а не код для виртуальной машины Java или .Net. И при этом язык Delphi очень прост и лаконичен, код на нем приятно читать и в нем достаточно легко разобраться, чего не могу сказать о коде на C или C++ (при всем моем великом уважении к разработчикам на C, хотя кто-то скажет, что это лишь дело привычки).
В настоящий момент Delphi утратил былую популярность. Вероятно, произошло это из-за того, что в 2000-х годах данный продукт был на несколько лет практически заброшен разработчиками, в результате чего он на какое-то время выпал из конкурентной гонки сред разработки. Действительно, после Delphi 7, выпущенного фирмой Borland в 2002-м году, более менее стабильный продукт появился лишь в 2007-м. Это был CodeGear Delphi 2007, выпущенный фирмой CodeGear, являющейся дочерней компанией Borland. Все версии между Delphi 7 и Delphi 2007 были практически непригодны к использованию. В 2008-м Borland продала подразделение CodeGear фирме Embarcadero Technologies, которая (за что ей особое спасибо!) незамедлительно начала превращать то, что ей досталось, в современную качественную среду разработки. Актуальной версией Delphi на момент написания статьи является Embarcadero Delphi XE2, выпущенная в сентябре 2011 года. Благодаря достаточно высокому качеству последних версий Delphi, данная среда разработки постепенно отыгрывает утраченные позиции.

Зачем нам многопоточность?


Люди хотели выполнять на компьютере несколько задач одновременно. Это называют многозадачностью. Реализуется многозадачность средствами операционной системы. Но если ОС умеет выполнять одновременно несколько приложений, почему бы и одному приложению внутри себя тоже не выполнять сразу несколько задач. Например, при архивации большого списка файлов архиватор может одновременно читать следующий файл, в это время в памяти архивировать текущий прочитанный и записывать результат в выходной файл на диске. Т.е. вместо того, чтобы в одном потоке выполнять над каждым файлом последовательно действия «прочитать» -> «заархивировать» -> «записать результат на диск», можно запустить 3 потока, один из которых будет читать файлы в память, второй поток — архивировать, а третий — сохранять на диск. Другим примером является выполнение в фоне какой-то малоприоритетной задачи — например, фоновое сохранение резервной копии файла, открытого в текстовом редакторе.
Если бы процессоры продолжали увеличивать свою тактовую частоту теми же темпами, как это происходило в 90-х и начале 2000-х годов, можно было бы не заморачиваться с многопоточностью и продолжать писать классический однопоточный код. Однако в последние годы процессоры перестали активно увеличивать скорость одного ядра, но зато начали наращивать количество самих этих ядер. Чтобы использовать потенциал современных процессоров на 100% без многопоточности просто не обойтись.

Почему сложно писать многопоточный код?


1) Легко допустить ошибку.
Когда на компьютере выполняется одновременно несколько приложений, адресное пространство (память) каждого процесса надежно изолировано от других процессов операционной системой и влезть в чужое адресное пространство довольно сложно. С потоками внутри одного процесса наоборот — все они работают с общим адресным пространством процесса и могут изменять его произвольным образом. Поэтому в многопоточном приложении приходится самостоятельно реализовывать защиту памяти и синхронизацию потоков, что приводит к необходимости написания относительно сложного, но при этом не несущего полезной нагрузки кода. Такой код называют «boilerplate» (сковородка), потому что сковородку надо сначала приготовить перед тем, как начнешь на ней что-то жарить. Именно необходимость написания «нестандартного» boilerplate-кода сдерживает развитие многопоточных вычислений. Для синхронизации потоков предусмотрено множество специальных механизмов: потокозащищенные (interlocked) команды процессора, объекты синхронизации операционной системы (критические секции, мьютексы, семаформы, события и т.п.), spin locks и т.д.
2) Код многопоточного приложения сложно анализировать.
Одна из сложностей многопоточного приложения состоит в том, что просматривая код многопоточного приложения визуально не понятно, может ли какой-то конкретный метод вызываться (и вызывается ли) из разных потоков. Т.е. вам придется держать в голове, какие методы могут вызываться из разных потоков, а какие нет. Поскольку делать абсолютно все методы потокозащищенными — это не вариант, всегда есть шанс нарваться на ошибку, вызвав из нескольких потоков метод, не являющийся потокозащищенным.
3) Многопоточное приложение сложно отлаживать.
В многопоточном приложении множество ошибок может возникать при определенном состоянии параллельно выполняющихся потоков (как правило, при последовательности команд, выполненных в разных потоках). Интересный пример описан тут (http://www.thedelphigeek.com/2011/08/multithreading-is-hard.html). Воссоздать такую ситуацию искусственно зачастую очень сложно, практически нереально. К тому же в Delphi инструментов для отладки многопоточных приложений не очень много, Visual Studio в этом плане явный лидер.
4) В многопоточном приложении сложно обрабатывать ошибки.
Если приложение имеет графический интерфейс пользователя, то взаимодействовать с пользователем может только один поток. Обычно, когда в приложении происходит какая-то ошибка, мы либо обрабатываем ее внутри приложения, либо показываем сообщение пользователю. Если же ошибка происходит в дополнительном потоке, он не может ничего сказать пользователю «немедленно». Соответственно, приходится сохранять ошибку, произошедшую в дополнительном потоке, до момента его синхронизации с основным потоком и лишь потом выдавать пользователю. Это может приводить к относительно сложной и запутанной структуре кода.

Есть ли способ хоть немного упростить себе жизнь?


Представляю вашему вниманию OmniThreadLibrary (сокращенно OTL). OmniThreadLibrary — это библиотека для создания многопоточных приложений в Delphi. Ее автор — Primoz Gabrijelcic из Словении — непревзойденный профессионал с многолетним стажем разработки приложений на Delphi. OmniThreadLibrary — это абсолютно бесплатная библиотека с открытыми исходными кодами. В настоящий момент библиотека находится уже в достаточно зрелой стадии и вполне пригодна для использования в серьезных проектах.

Где найти информацию по OTL?
Также автор библиотеки сейчас занимается наполнением wiki-книги про OmniThreadLibrary и многопоточность, уже готовы статьи про большинство высокоуровневых примитивов OTL.

Какие возможности предоставляет OTL?


Данная библиотека содержит в себе низкоуровневые и высокоуровневые классы, позволяющие упрощенно управлять многопоточностью, не вдаваясь в подробности процессов создания/освобождения/синхронизации потоков на уровне WinAPI.
Особенный интерес представляют высокоуровневые примитивы для упрощенного управления многопточностью. Они примечательны тем, что их сравнительно легко интегрировать в готовое однопоточное приложение, практически не меняя структуры исходного кода. Данные примитивы позволяют создавать многопоточные приложения, концентрируясь на полезном коде приложения, а не на вспомогательном коде для управления многопоточностью.
К основным высокоуровневым примитивам относятся Future (асинхронная функция), Pipeline (конвейер), Join (параллельный вызов нескольких методов), ForkJoin (рекурсия с параллелизмом), Async (асинхронный метод), ForEach (параллельный цикл).
На мой взгляд, самыми интересными и полезными примитивами являются Future и Pipeline, т.к. для их использования имеющиеся код почти не нужно переписывать.

Future


Данный примитив позволяет выполнить асинхронный вызов функции и в нужный момент дождаться завершения расчета и получить результат выполнения. С помощью данного примитива вызов любой процедуры или функции можно безболезненно превратить в асинхронный.
Выглядит это примерно так:

uses
  OtlParallel;
...
procedure TestFuture;
var
  vFuture: IOmniFuture<integer>; 
begin
  // Запускаем вычисления в параллельном потоке
  vFuture := Parallel.Future<integer>(
    function: integer
    var
      i: integer;
    begin
      Result := 0;
      for i := 1 to 100000 do
        Result := Result + i;
    end
  );
// Здесь делаем какие-то вычисления в основном потоке (в это время параллельный поток работает (он может еще не запустился, а может уже завершился, но мы об этом ничего пока не знаем)
// Теперь нам понадобилось узнать результат, полученный в параллельном потоке
  ShowMessage(IntToStr(vFuture.Value));
end;


Обратите внимание, что именно обращение к vFuture.Value является моментом синхронизации основного потока с дополнительным, т.е. до тех пор, пока мы не обратимся к Value мы вообще ничего не знаем о состоянии другого потока. Как только мы вызвали Value, основной поток приостанавливается до момента завершения расчета в дополнительном потоке.

Если требуется, можно в основном потоке реализовать неблокирующее ожидание результата:
while not vFuture.IsDone do
  Application.ProcessMessages;  

Таким образом, примитив Future позволяет асинхронно выполнить какую-то задачу и вернуть результат в основной поток именно в тот момент, в который он там потребуется.

Pipeline


Pipeline (конвейер) — это гораздо более мощный примитив по сравнению с Future.
Представьте, что некий алгоритм выполняется в цикле для множества элементов. Например, производится какая-то обработка файлов в каталоге. Однопоточная программа будет брать очередной файл, прочитывать его, выполнять какие-то действия и сохранять измененный файл на диск. Имея конвейер можно исходный алгоритм разделить на этапы (чтение, обработка, сохранение) и запустить эти этапы в параллельных потоках. В самом начале запустится лишь самый первый этап и прочитает первый файл. Как только чтение завершится, запустится второй этап и начнет обработку прочитанного файла или его порции (если первый этап читает файлы не целиком а порциями). В это время первый этап уже начнет читать второй файл. Как только второй этап обработает первый файл, подключится третий этап и начнет сохранение. В этот момент мы получим состояние, при котором все три этапа работают параллельно.
Пример для Pipeline, близкий к реальной жизни, слишком загрузил бы статью, поэтому для иллюстрации использования Pipeline ограничиваюсь копией абсолютно синтетического примера из OtlBook (чур сильно не бить!):

uses
  OtlCommon,
  OtlCollections,
  OtlParallel;
 
var
  sum: integer;
 
begin
  sum := Parallel.Pipeline
  .Stage(
    procedure (const input, output: IOmniBlockingCollection)
    var
      i: integer;
    begin
      for i := 1 to 1000000 do
        output.Add(i);
    end)
  .Stage(
    procedure (const input: TOmniValue; var output: TOmniValue)
    begin
      output := input.AsInteger * 3;
    end)
  .Stage(
    procedure (const input, output: IOmniBlockingCollection)
    var
      sum: integer;
      value: TOmniValue;
    begin
      sum := 0;
      for value in input do
        Inc(sum, value);
      output.Add(sum);
    end)
  .Run.Output.Next;
end;

В данном примере первый этап генерирует миллион чисел, передавая их по одному на следующий этап. Второй этап умножает каждое число на 3 и передает на третий этап. Третий этап суммирует результаты и возвращает одно число. Каждый Stage выполняется в своем потоке. Более того, Otl позволяет указать, какое число потоков для каждого Stage'а использовать (если одного мало) за счет простого модификатора .NumTasks(N). Возможности OTL действительно очень широки.

Базовым классом для поддержки обмена данными между этапами конвейера является класс потокозащищенной очереди — TOmniBlockingCollection. Данный класс позволяет нескольким потокам одновременно добавлять и считывать элементы. Высокая скорость работы коллекции достигается за счет хитрого управления памятью и применения блокировок на основе потокозащищенных инструкций процессора вместо блокировок на основе объектов синхронизации ОС. О подробностях реализации класса TOmniBlockingCollection можно почитать тут, тут и тут.

Заключение


Кто-то посмотрев на приведенные примеры скажет «да я все это уже видел». Действительно, в Task Parallel Library для .Net Framework 4 присутствуют примерно такие же классы. При этом существует ряд различий между тем, как исполняются потоки внутри машины .Net и как потоки исполняются на реальном процессоре. Рассмотрение данных различий — за пределами этой статьи. Я лишь хотел акцентировать внимание на замечательной библиотеке, и тех широких возможностях которые она предоставляет Delphi разработчикам. Хочу отметить, что библиотека снабжена большим количеством примеров, иллюстрирующих использование как низкоуровневых, так и высокоуровневых классов.

Чтобы развеять опасения по поводу зрелости и надежности данной библиотеки, скажу лишь, что за счет использования Pipeline в сложном коммерческом многопользовательском приложении (не web) удалось сократить время выполнения операции над группой файлов на клиенте почти в два раза за счет разнесения по отдельным потокам обработку файлов на клиенте и их передачу на сервер. Использовать ли связку Delphi + OmniThreadLibrary в ваших проектах — решать вам ;)
Джон Смит @alan008
карма
59,0
рейтинг 0,1
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (60)

  • –18
    Я программирую на Delphi очень давно и не перестаю наслаждаться.

    Закопайте стюардессу.
    • 0
      А чем вас не устроил стандартный TThread?
      • –3
        Это вы мне?
      • +1
        TThread никуда не девается. TThread — это обертка над базовыми WinAPI функциями. Более того, на нижнем уровне библиотека OTL класс TThread успешно используется. Здесь же речь идет об уровнях гораздо более высоких по сравнению с TThread. Вы попробуйте сначала реализовать на основе голого TThread, например, конвейер (Pipeline) и тогда поймете, в чем разница.
    • 0
      Извините, промахнулся. Вопрос адресован ТС
    • +1
      Ну например когда я еще увлекался программизмом — тоже обожал Дельфи. Красивый язык, с которым не приходится изобретать велосипеды, но надо под каждый чих качать компоненты. Его идеологию ИМХО перенял C#.
      • –3
        Красивый язык, с которым не приходится изобретать велосипеды

        Был в свое время, версии эдак до 6-й.

        Его идеологию ИМХО перенял C#.

        Внезапно, потому что их проектировал один и тот же человек. Собственно, примерно с момента как Хейльсберг ушел в Майкрософт, Дельфи и покатился по наклонной.
        • +4
          Embarcadero Delphi XE/XE2 качественные, удобные, современные продукты. «Дельфи покатился по наклонной» можно было говорить этак году в 2006-м, но уж точно не сейчас.
  • –14
    Его уникальность в том, что он одновременно позволяет создавать код сколь угодно высокого уровня, при этом оставаясь «близким к железу», т.к. на выходе мы получаем native-приложение, а не код для виртуальной машины Java или .Net.

    Ни в Java, ни в .Net нет никаких виртуальных машин. Там есть JIT-компиляторы, которые работают не медленнее дельфовского RTTL.
    • –7
      * RTTI, конечно.
    • –7
    • +1
      а что такое CLR?
      • 0
        Управляемое окружение.
        • +3
          коды на языке C#, например, компилируются в промежуточный байт-код (MSIL).
          далее 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 когда-то была действительно полноценная виртуальной машина.
  • +2
    Надо было в статье еще об AsyncCalls упомянуть.
    • 0
      Точно, спасибо, совсем он выпал у меня из головы. Но в OmniThreadLibrary, безусловно, возможностей _гораздо_ больше.
  • 0
    Есть еще JEDI Core и JEDI VCL и там набор всевозможных компонентов JvThread, JvThreadTimer и т.д.
    • 0
      Еще раз акцентирую внимание читателей (видимо в статье не получилось поставить этот акцент), OmniThreadLibrary предоставляет гораздо более высокоуровневый подход к написанию многопоточного кода по сравнению с WinAPI и обертками типа TThread. Из приведенных выше примеров (какими бы примитивными они ни были) видно, что ни методов создания/уничтожения потоков, ни функций ожидания/синхронизации явным образом вызывать не нужно, все спрятано за очень красивыми и удобными абстракциями.
      • 0
        >>видно, что ни методов создания/уничтожения потоков, ни функций ожидания/синхронизации явным образом вызывать не нужно, все спрятано за очень красивыми и удобными абстракциями.

        А есть примеры более сложных программ с использованием OmniThreadLibrary?

        Интересует примеры:
        1. Работа нескольких потоков с одним массивом данных.
        2. Доступ и работа с БД из нескольких потоков.
        3. Работа нескольких потоков по расчету каких-либо данных и вывод информации на форму, к примеру построение графиков.
        • 0
          >>1.Работа нескольких потоков с одним массивом данных.
          Слишком общее описание вопроса, непонятно, какую именно задачу вы решаете.
          >>2.Доступ и работа с БД из нескольких потоков.
          Здесь все очень тонко. Если у вас один Connection, то вы сможете использовать его только в одном потоке. Если создавать Connection для каждого потока, который может обращаться к базе, то появляется риск нарваться на блокировки с самим собой не уровне СУБД.
          >>3.
          Опять же слишком общее описание.

          OTL снабжена значительным количеством примеров, позволяющих быстро разобраться с основными идеями и начать применять библиотеку на практике.
          • 0
            >>OTL снабжена значительным количеством примеров, позволяющих быстро разобраться с основными идеями и начать применять библиотеку на практике.

            В папке examples всего один пример: stringlist parser :(
            Есть конечно много чего в папке tests, Вы про это говорите?
            • 0
              Да, именно про tests. Примеры, использующие высокоуровневые контрукции находятся ближе к концу списка. Лучше вообще эти примеры просматривать от конца к началу :)
              • 0
                Понятно, спасибо, буду изучать.
  • –5
    >И при этом язык Delphi очень прост и лаконичен, код на нем приятно читать и в нем достаточно легко разобраться, чего не могу сказать о коде на C или C++

    Вот не скажи, одни только begin и end как код раздувают по размеру, да и синтаксис объявления функций весьма громоздкий. Для обучения хорошо, а тут только строчки лишние плодятся.
    Ну и в плюсах тоже есть std::future в аналогичным функционалом. Да и в Qt уже столет в обед она была.
  • +3
    Вот не скажи, одни только begin и end как код раздувают по размеру, да и синтаксис объявления функций весьма громоздкий.

    Это никак не противоречит «код на нем приятно читать и в нем достаточно легко разобраться».
  • 0
    FYI. Мне кажется, что это — лучшая статья про потоки в Delphi. Думаю, что перед тем как пользоваться оболочками в виде OmniThreadLibrary, надо понимать основы.
    • 0
      Ага, читал. Отличная статья на момент ее написания. Когда попробовал OTL, понял что многими низкоуровневыми вещами для написания «первого многопоточного приложения» можно голову не забивать. OTL позволяет концентрироваться на полезном коде приложения, а не коде для поддержки запуска, остановки, уничтожения, синхронизации потоков.
      • 0
        Для первого и простого можно. Но если я напишу что-нибудь сложное с OTL, и в процессе тестирования вылезут гейзенбаги, то я бы хотел знать основы, чтобы хотя бы предположить, что пошло не так. Многопоточность не настолько простая область, имхо, чтобы совсем про нее не читать и довериться сторонним библиотекам.
  • 0
    Большое спасибо за статью!
    Заинтересовало…

    Присоединяюсь к просьбе рассмотреть пример: доступ и работа с БД из нескольких потоков.

    А именно
    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 в угловых скобках? :-) Или как это называется, и где про это почитать? :-)
    • +1
      Это назвается generics (generic types, дженерики или обобщенные типы). Появились они, если память мне не изменяет, в Delphi 2009.
      • 0
        Благодарю :-)
    • +1
      Почитать можно тут.
  • 0
    Похоже на адаптацию .NET'ного Thread Pool'а и механизма асинхронного вызова делегатов Delegate.BeginInvoke, работающего на этом пуле, а так же от части WPF'ного Dispatcher'а. В C# 4.5 будет готовый синтаксический сахар для всех этих дел — ключевые слова async и wait.
    Что касается чисто Delphi, многопоточность там приемлема, пока дело не доходит до взаимодействия с GUI. На моей практике в подавляющем большинстве случаев задача заключалась в необходимости запустить асинхронную операцию с выводом процента выполнения в GUI. А до GUI из фоновых потоков можно добраться только через одно место — очередь оконных сообщений основного потока (SendMessage, PostMessage), и через набор обработчиков этих сообщений с другой стороны. А всё по тому, что кое-кто не озаботился реализацией alertable message loop'а в VCL, что не позволяет производить «инъекции» своих методов обратного вызова в поток, обрабатывающий оконные сообщения через APC. А это могло бы существенно упростить жизнь.
    • 0
      Вообще то стандартный метод синхронизации потоков с GUI это 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);
      • 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
              Это не лучше или хуже это просто по-другому. Иногда надо блокировать, иногда не надо. Есть же упомянутый Queue, который делает тоже самое.
              • 0
                Ну вот Invoke это примерно то же самое, что Queue, только в терминах фреймворка библиотеки OTL.
    • 0
      Насколько помню, в VCL все в порядке с message loop, просто нужно немного понимать, как он работает.

      Тут уже упомянули про Synchronize() — это банально выполнение метода (процедуры) в основном потоке, перед переходом в idle. Дешево и сердито, но глючный поток может поломать всю программу.

      Правильнее было бы создание простейшей глобальной thread-safe очереди сообщений на базе TThreadList, и обрабатывать ее по таймеру. Тогда код дочерних потоков будет полностью изолирован от кода основного потока, без лишних блокировок и заморочек. Даже если поток сломается или зависнет — прога просто поймает исключение в TApplicationEvents.OnException() и продолжит полет.
      • 0
        Да, в VCL c message loop действительно все в порядке. Имелась в виду ситуация
        procedure A;
        begin
          // Запускаем доп. поток
          // ... Что-то делаем ... В это время второй поток хочет сделать Synchronyze
          // Ждем завершения второго потока. Вот здесь надо делать ProcessMessages вручную, иначе можно ждать бесконечно
        end;
        


        Обработку сообщений по таймеру и «правильнее было бы» даже комментировать не буду.
  • 0
    Нежно любил Дельфю-7 за простой и читабельный синтаксис, удобные библиотеки и компоненты.
    В новых версиях синтаксис испортился, при этом мало что улучшилось. Ну и хрен с ними.
    А теперь открыл для себя Python, чего и вам желаю!
    • 0
      >В новых версиях синтаксис испортился
      Это чем же он испортился интересно. Дженериками и анонимными методами? ;)
      • 0
        Оними самыми. Хотели как лучше, а получилось как у всех.
        • 0
          Не вижу причин для недовольства. Отличный функционал, ИМХО.
          • 0
            Функционал, безусловно, нужный. Но реализован как калька с С-образных языков, где вместо понятных человеку слов и символов — всякие галочки, точки, кавычки и прочие элементы таблицы ASCII, которые несут в себе особый внутренний смысл.
        • 0
          TObjectList вместо TObjectList это уже прорыв (кроме шуток)
          • 0
            На мой взгляд это уродливый костыль для обхода ограничений типизации.
            • 0
              Это не обход ограничений типизации, а наоборот, усиление типизации. Обычный TObjectList может хранить объекты любого (абсолютно любого) класса-наследника TObject. А «TObjectList угловая_скобка TPerson угловая_скобка» может хранить только объекты класса TPerson и его наследников, что позволяет не делать каждый раз преобразование «Items[i] as TPerson», а сразу обращаться к Items[i] как к TPerson.
              • 0
                А могло быть и так:

                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
          Имел в виду «TObjectList угловая скобка T угловая скобка» вместо TObjectList (хабр вырезает их)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.