Topic: Simple multithreading TWThread, TWCThread

Планы, идеология, архитектура и т.п.

Модератор: Модераторы

Topic: Simple multithreading TWThread, TWCThread

Сообщение wadman » 27.10.2016 16:09:45

Решил поделиться своими наработками по теме.

Библиотека позволяет работать с доп. потоками в привычном для делфи режиме в стиле компонентов.
Проверена работа в следующих сочетаниях: WinCE 5, Windows XP/7, Ubuntu 14, 16LTS, Delphi 7/XE2/XE3, Lazarus 1.6, 1.7.
Некоторые пользователи успешно проверили так же на Win10, OpenSuse. На FreeBSD не взлетело, пока возможности исправить это у меня нет.

Можно обойтись и без компоненты TWChread, воспользовавшись классом TWThread, оберткой над которым компонент и является.
Суть компоненты проста: TWCThread - это поток, который включает в себя любое количество TTAsk, которые выполняются в контексте этого пока.
Вкратце TTask это сообщение для потока. И отработают таски именно в том порядке, в котором их запустить.

Так же добавлен модуль для безопасного логирования в многопоточном окружении (wlog).

У TTask объявлены следующие события:
OnExecute - это событие выполняется в контексте дополнительного потока. Отсюда лучше не трогать VCL/LCL, либо с пониманием и осторожностью.
В него передаются const Msg и var Param (variant) от процедуры Start.
OnFinish - выполняется по завершению OnExecute в контексте основного потока. Здесь так же имеются параметры const Msg и const Param, которые связаны с OnExecute. Их можно анализировать (в OnExecute параметр можно менять, а здесь анализировать).
OnProgress - выполняется в основном потоке. см PostProgress. Есть параметр Msg.
OnMessage - выполняется в основном потоке. см PostMessage. Есть параметры Msg и Param (variant).

У потока можно задавать AffinityMask и менять приоритет.

Перед уничтожением формы, где расположен компонент потока необходимо выполнять этот код:
Код: Выделить всё
procedure TfrmMain.FormDestroy(Sender: TObject);
begin
    WCThread1.FinishAllTasks;

Который прекращает работу текущей задачи (если таковая имеется) и ждет её аварийного завершения.

В архиве так же есть несколько демок.

Для демки фоновой генерации картинок (эмуляция фоновой печати) в модуль LR_Class.pas пакета laszReport необходимо внести некоторые правки.
А именно добавить опцию roMultithread, которая позволит отключить вызов Application.ProcessMessages из других потоков.
После добавления опции так же нужно добавить процедуру procedure DoApplicationProcessMessages;
Код: Выделить всё
procedure TfrReport.DoApplicationProcessMessages;
begin
  if (not (roMultithread in Options)) or (csDesigning in ComponentState) then
    Application.ProcessMessages;
end;

И во всем модуле заменить вызов Application.ProcessMessages на следующий код:
Код: Выделить всё
if Assigned(CurReport) then
  CurReport.DoApplicationProcessMessages;

В архиве имеется готовый модуль LR_Class.pas, который является последним на данный момент (27.10.2016) взятым с github из пакета lazReport.
Теперь после отключения ShowProgress и выставления roMultithread ваш отчет может формироваться в другом потоке. :)

[28.10.2016]
LazReport (демка) прекрасно работает в Windows, а в никсах - никак.
Поправил пару мелких баков.

[13.04.2017]
Убрал StopThread (Terminate удобнее).
Добавил возможность стартовать поток 2 из потока 1 с обратной связью в поток 1 (а не VCL/LCL).
Тут нужно понимать, что используется механизм сообщений и обратная связь идет через очередь потока 1.
Убрал жуткую ошибку, которая тянулась с самого начала, но вылезла только на стресс-тесте в внезапным закрытием, когда потоки во всю "общаются".
Ну и исправления по мелочи...

[10.07.2017]
Убрал архив, залил на гитхаб: https://github.com/wadman/wthread/
Из последних изменений: поток для синхронного вызова vcl/lcl перевел в режим singleton.
Как раньше не догадался? При большом количестве потоков большой и не нужный расход памяти и дескрипторов.
Теперь таски по умолчанию принудительно прибиваются при Destroy (выставлением соответствующего флага, в таске это проверяется Terminated), если не вызвать явно ожидание их работы (FinishAllTasks, WaitAllTasks). Раньше было наоборот.
Последний раз редактировалось wadman 10.07.2017 15:23:56, всего редактировалось 2 раз(а).
wadman
постоялец
 
Сообщения: 122
Зарегистрирован: 18.10.2016 15:54:28

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение serbod » 28.10.2016 15:40:13

Непонятна разница между обычным TThread и TWThread. Судя по исходникам, там вовсю используются сообщения, аналогичные виндовым TMessage. При этом непонятно, нафига они нужны, если их обработчик вызывается в основном потоке. Лучше тогда использовать стандартные виндовые, их можно к методам классов привязывать. И использовать стандартные PostMessage(), Application.ProcessMessages() вместо непонятных суррогатов.

Еще непонятно насчет взаимоблокировки потоков или атомарной модификации данных. Обычно это сложный вопрос из-за обилия стандартных способов (TCriticalSection, TEvent, TMultiReadExclusiveWriteSynchronizer плюс куча функций). Новый компонент не предлагает ничего на этот счет?

Добавлено спустя 5 минут 36 секунд:
Еще зачем-то используется модуль Variants, довольно "тяжелый" и востребованный в основном для работы с базами данных. Лучше уж обычный AnsiString использовать, он изначально доступен и применяется повсеместно.
Аватара пользователя
serbod
постоялец
 
Сообщения: 449
Зарегистрирован: 16.09.2016 11:03:02
Откуда: Минск

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение wadman » 28.10.2016 16:00:21

serbod писал(а):При этом непонятно, нафига они нужны, если их обработчик вызывается в основном потоке.

Сообщения, принятые в доп.потоке в нем и обрабатываются. Сообщения, принятые в основном потоке обрабатываются, соответственно, в основном.
serbod писал(а):Еще зачем-то используется модуль Variants, довольно "тяжелый" и востребованный в основном для работы с базами данных.

Для передачи параметров туда-сюда между основным потоком и дополнительным.
serbod писал(а):Application.ProcessMessages()

Этим вообще нельзя пользоваться в многопоточке.

Добавлено спустя 3 минуты 49 секунд:
serbod писал(а):Непонятна разница между обычным TThread и TWThread.

Вкратце разница в том, что поток спит до сообщения, обрабатывает его (может шлет в основной какие-то сообщения или результаты своей работы) и засыпает до следующего сообщения.
wadman
постоялец
 
Сообщения: 122
Зарегистрирован: 18.10.2016 15:54:28

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение runewalsh » 28.10.2016 16:45:00

serbod писал(а):Лучше тогда использовать стандартные виндовые, их можно к методам классов привязывать.

Здесь вроде тоже привязываются. Если уж придираться, то наиболее естественная реализация — не явный поток, крутящий очередь сообщений, а пул, уже сам решающий, когда и сколько потоков создавать, и раскидывающий по ним задания. Честно говоря, слабо представляю, что нужно для счастья, кроме QueueUserWorkItem ван лав ♡.

Обработчики вызываются из основного потока потому, что предполагается, что в них пользователь будет обновлять GUI (например, OnProgress рисует полоску прогресса, OnFinish — что-то мутит с результатом), а VCL&LCL основаны на древней парадигме, предполагающей, что GUI занимается главный поток. TThread.Synchronize предназначена ровно для той же цели, но в предусмотренных здесь случаях не требуется.
Аватара пользователя
runewalsh
энтузиаст
 
Сообщения: 578
Зарегистрирован: 27.04.2010 00:15:25

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение wadman » 28.10.2016 16:58:09

runewalsh писал(а):TThread.Synchronize предназначена ровно для той же цели, но в предусмотренных здесь случаях не требуется.

В LCL нет аналога AllocateWnd (и уж тем более для юниксов), потому тут очередь организована через синхронизацию, но чтобы избежать тормозов за этим делом следит второй скрытый поток. Именно он синхронизируется с GUI.
wadman
постоялец
 
Сообщения: 122
Зарегистрирован: 18.10.2016 15:54:28

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение serbod » 28.10.2016 17:32:39

То есть, внутри доп.потока обрабатываются сообщения GUI? И есть обработчик, вызываемый из потока? Это чтобы однажды не вспомнить об этом факте и в обработчике затронуть визуальные объекты LCL?

Так нельзя. Еще сам Borland завещал, что TThread нужно описывать в отдельном модуле, где не используются классы VCL, а в инициализации заранее включена многопоточность менеджера памяти (IsMultiThread := True). Вот только на уровне IDE/компилятора это не сделали.

Сама идея управлять потоками при помощи сообщений правильная, но не надо для этого использовать сообщения Windows/LCL. А сообщить о завершении или прогрессе потока можно и без синхронизации, а сразу запульнуть нужное сообщение из потока и поймать его компонентом, как это делает TTimer.
Аватара пользователя
serbod
постоялец
 
Сообщения: 449
Зарегистрирован: 16.09.2016 11:03:02
Откуда: Минск

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение wadman » 28.10.2016 17:48:01

serbod писал(а):То есть, внутри доп.потока обрабатываются сообщения GUI?

То есть внутри скрытого потока обрабатывается синхронизация с GUI. Это сделано для того, чтобы основной доп.поток не тормозился обработкой синхронизацией с GUI.
Это эмуляция очереди сообщений для fpc. Для делфи сделано красиво и через сообщения.
serbod писал(а):Так нельзя.

Я сделал как можно и оттестировал на различных сценариях. Потому и делюсь наработками.
serbod писал(а):Сама идея управлять потоками при помощи сообщений правильная, но не надо для этого использовать сообщения Windows/LCL.

Как надо, модуль сам определяет на уровне ifdef. Где - сообщения, а где - еще один поток, симулирующий очередь сообщений.
Чтоб на всех платформах было одинаково и переносимо без изменения кода.
wadman
постоялец
 
Сообщения: 122
Зарегистрирован: 18.10.2016 15:54:28

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение serbod » 30.10.2016 16:50:43

Нельзя, чтобы обработчик события контрола вызывался вне основного потока. Вы это прекрасно понимаете.
wadman писал(а):У TTask объявлены следующие события:
OnExecute - это событие выполняется в контексте дополнительного потока. Отсюда лучше не трогать VCL/LCL, либо с пониманием и осторожностью.

Для многих это совсем неочевидно, нет инстиктивного понимания и осторожности. Оставлять такую возможность выстрелить в ногу просто незачем. От нее будет больше вреда, чем пользы. Ведь выигрыш производительности при этом мизерный, а проблемы возникнут с 1% вероятностью, то есть у разработчика может и не глюкнет, а у заказчика будет глючить внезапно и непредсказуемо. Поэтому я против любых компонентов типа TThreadTimer, ранних TIdSocket и других, где обработчик события вызывается из неосновного потока.

Написать потомка TThread по времени на полчаса дольше, чем вставить компонент, но на поиск и устранение ошибки в обработчике события компонента может уйти полгода. Пока в языке нет средств сделать блоки кода потокобезопасными, лучше не рисковать.

Я бы порекомендовал сделать мастер создания фонового потока (как в Delphi), который создает в отдельном файле потомок TThread с обвязкой для разных сценариев использования и комментариями, что можно делать и что нельзя. У него будут все шансы попасть в состав Lazarus.
Аватара пользователя
serbod
постоялец
 
Сообщения: 449
Зарегистрирован: 16.09.2016 11:03:02
Откуда: Минск

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение wadman » 30.10.2016 20:29:23

serbod писал(а):Нельзя, чтобы обработчик события контрола вызывался вне основного потока. Вы это прекрасно понимаете.

Верно. Но не понимаю, что вы вообще пытаетесь мне донести?

Остальную воду пропустил, потому что топчемся на одном месте и обсуждаем непонятно что.
Если не нравится решение, которое не попробовали и о работе которого нет понимания, то можно его не использовать.

Добавлено спустя 1 минуту 36 секунд:
Либо уже покажите код, где у вас что-то не получается.
wadman
постоялец
 
Сообщения: 122
Зарегистрирован: 18.10.2016 15:54:28

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение runewalsh » 30.10.2016 23:09:03

serbod писал(а):Оставлять такую возможность выстрелить в ногу просто незачем.

Странно звучит в контексте языка, не защищающего от порчи памяти или двойного вызова деструктора. ;)

Вот небольшой тест. Самое ужасное здесь — не базовая разница в скорости, с ней-то жить можно, а то, что действия, которые в общем случае не имеют отношения к GUI, оказываются привязанными к циклу сообщений. Тяжёлый контрол (в моём примере эмулируется таймером, который каждые 150 мс из 200 спит в OnTimer) замедлит ВСЕ обработчики, полагающиеся на выполнение из основного потока, а ведь пока они находятся в очереди сообщений, вызвавшие их потоки также будут простаивать. Ну и зачем многопоточность с таким подходом?

Если пользователь стреляет по ногам, у него тысяча и одна возможность на что-нибудь напороться, и ради того, чтобы убрать одну, ты предлагаешь штрафовать правильный код.
Вложения
SyncBench.rar
(66.33 КБ) Скачиваний: 610
Аватара пользователя
runewalsh
энтузиаст
 
Сообщения: 578
Зарегистрирован: 27.04.2010 00:15:25

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение serbod » 31.10.2016 10:45:27

runewalsh писал(а):Странно звучит в контексте языка, не защищающего от порчи памяти или двойного вызова деструктора. ;)

При включенных предупреждениях компилятор будет ругаться на большинство конструкций, способных испортить память. А в рантайме есть проверки на выход за пределы диапазона. С объектами после деструкции беда, согласен. Но тоже лечится использованием интерфейсов вместо классов, FreeAndNil(), фантомные объекты в менеджере памяти. А для многопоточности никаких проверок и предупреждений.

runewalsh писал(а):Если пользователь стреляет по ногам, у него тысяча и одна возможность на что-нибудь напороться, и ради того, чтобы убрать одну, ты предлагаешь штрафовать правильный код.


Я предлагаю уменьшить вероятность выстрелить в ногу для новичков. Пусть стреляют если хотят, но для этого нужно сознательно снять предохранитель или собрать свой пистолет. В Дельфях есть мастер создания Thread Object, который добавляет такой комментарий:
Код: Выделить всё
{ Important: Methods and properties of objects in visual components can only be
  used in a method called using Synchronize, for example,

      Synchronize(UpdateCaption);

  and UpdateCaption could look like,

    procedure aaa.UpdateCaption;
    begin
      Form1.Caption := 'Updated in a thread';
    end; }


Его трудно не заметить, даже если не читать документацию.
Аватара пользователя
serbod
постоялец
 
Сообщения: 449
Зарегистрирован: 16.09.2016 11:03:02
Откуда: Минск

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение wadman » 13.04.2017 10:39:22

Обновил...

Кто-нибудь пользуется хоть? :)
wadman
постоялец
 
Сообщения: 122
Зарегистрирован: 18.10.2016 15:54:28

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение sts » 13.04.2017 11:07:23

Тут бы отыскать кто лазарусом пользуется...
sts
постоялец
 
Сообщения: 406
Зарегистрирован: 04.04.2008 12:15:44
Откуда: Тольятти

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение Лекс Айрин » 13.04.2017 13:34:12

sts, да ладно тебе... еще как пользуются)))
Аватара пользователя
Лекс Айрин
долгожитель
 
Сообщения: 5723
Зарегистрирован: 19.02.2013 16:54:51
Откуда: Волгоград

Re: Topic: Simple multithreading TWThread, TWCThread

Сообщение wadman » 10.07.2017 15:24:47

Убрал архив, залил на гитхаб: https://github.com/wadman/wthread/ (шапку темы обновил)
Из последних изменений: поток для синхронного вызова vcl/lcl перевел в режим singleton.
Как раньше не догадался? При большом количестве потоков большой и не нужный расход памяти и дескрипторов.
Теперь таски по умолчанию принудительно прибиваются при Destroy (выставлением соответствующего флага, в таске это проверяется Terminated), если не вызвать явно ожидание их работы (FinishAllTasks, WaitAllTasks). Раньше было наоборот.
wadman
постоялец
 
Сообщения: 122
Зарегистрирован: 18.10.2016 15:54:28

След.

Вернуться в Разработки на нашем сайте

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 5

Рейтинг@Mail.ru