В последнее время я исследую возможности выполнения параллельных и асинхронных операций в Delphi. В качестве одного из результатов родился класс TAwaitable. Код нарочито простой. С одной стороны хочется продемонстрировать идею, хотя она в целом очевидна, с другой стороны — получить свою порцию конструктивной критики. Я довольно мало сталкивался с многопоточностью в Delphi, так что будет здорово, если кто-то укажет на недочеты в реализации, если они есть.
Собственно, для большей наглядности сначала пример использования, а затем код самого класса.
procedure TForm1.Button1Click(Sender: TObject); var WaitFor2, WaitFor3: IAwaitable<Integer>; Log: string; begin Memo1.Lines.Add(DateTimeToStr(Now) + ' Start testing...'); WaitFor2 := TAwaitable<Integer>.Create( function : Integer begin Sleep(2000); Result := 2; end); WaitFor3 := TAwaitable<Integer>.Create( function : Integer begin Sleep(3000); Result := 3; end); Memo1.Lines.Add(DateTimeToStr(Now) + ' One more action'); Log := IntToStr(WaitFor2.Value + WaitFor3.Value); Memo1.Lines.Add(DateTimeToStr(Now) + ' And finally the result is... ' + Log); Log := IntToStr(WaitFor2.Value + WaitFor3.Value); Memo1.Lines.Add(DateTimeToStr(Now) + ' Cached result... ' + Log); end;
Здесь запускаются в параллельное выполнение две функции. Одна выполняется 2 секунды, другая — 3. И в одном случае результат вычислений — 2, а в другом соответственно — 3. В качестве итога нам нужна сумма двух этих значений. Последовательное вычисление заняло бы, очевидно, 5 секунд. Но данный код выполняется за 3 секунды, благодаря тому, что он выполняется параллельно.
19.11.2013 23:33:04 Start testing...
19.11.2013 23:33:04 One more action
19.11.2013 23:33:07 And finally the result is... 5
19.11.2013 23:33:07 Cached result... 5
Повторное обращение к значению TAwaitable.Value выполняется быстрее, так как вычисления уже завершены.
Код самого класса довольно прост.
unit Awaitable; interface uses System.Classes; type TAwaitableProc<T> = reference to function : T; IAwaitable<T> = interface function Value: T; end; TAwaitable<T> = class(TInterfacedObject, IAwaitable<T>) private FThread: TThread; FValue: T; public constructor Create(AwaitableProc: TAwaitableProc<T>); reintroduce; destructor Destroy; override; function Value: T; end; implementation { TAwaitable<T> } constructor TAwaitable<T>.Create(AwaitableProc: TAwaitableProc<T>); begin inherited Create; FThread := TThread.CreateAnonymousThread( procedure begin FValue := AwaitableProc; end); FThread.FreeOnTerminate := False; FThread.Start; end; destructor TAwaitable<T>.Destroy; begin FThread.Free; inherited; end; function TAwaitable<T>.Value: T; begin FThread.WaitFor; Result := FValue; end; end.
Ну вот как-то так. Этот класс есть куда функционально развивать, но пока не хочется заходить слишком далеко.
update
День не прошел зря, я понял свой недочет. Вместо критических секций лучше использовать уже имеющийся в классе TThread метод WaitFor. Поэтому я немного изменил код.
1. Николай Зверев
21 Ноя 2013 1:05 пп
Да, пример интересный. И, что главное, простой. Это безусловно плюс :)
Но меня вот больше интересует не сам вопрос «как распараллелить», а интересны случаи, когда это можно (и следует) использовать, а когда лучше и не заморачиваться с потоками. Ведь многое зависит от исходной задачи и от задействованных в вычислениях ресурсов…
2. Роман Янковский
21 Ноя 2013 2:20 пп
В последнее время мне все больше кажется, что многопоточность на фронтенде (а Delphi это все-таки как правило фронтенд) нужна в первую очередь не для ускорения вычислений или более эффективного использования ресурсов (хотя это важно, конечно), а для реализации асинхронных действий в UI.
Намного приятнее пользоваться программой, которая не блокирует весь интерфейс при выполнении каждой операции, пусть даже эти операции в итоге займут столько же времени, сколько занимают их синхронные аналоги. Пользователю не так важно, что операция занимает полчаса, ему просто хочется продолжить работу не дожидаясь ее завершения.
То есть первые кандидаты на асинхронность — это длительные, но не ресурсоемкие действия. С ресурсоемкими действиями сложнее — что-то «тяжелое» работающее в фоне может помешать работе всего приложения.
Конкретные случаи — это такая глобальная тема. Если что-то в голове сформируется подходящее для блога, я постараюсь написать.
3. Марин Мирою
21 Ноя 2013 11:32 пп
— Например: выполнение запросов к БД, генерация отчётов, операции импорта и экспорта данных, поиск информации (особенно, если он — основан на переборе), формирование индексов.
IMHO показано делать асинхронным всё, что блокирует интерфейс программы на заметное время.
Когда «не заморачиваться»:
* если операция не блокирует интерфейс надолго
* если вынесение обработки в нить затруднено по разными причинам
При прочих равных условиях, асинхронность — «вишенка на торте». Далеко не всегда её просто обеспечить. Например, AnyDAC не умеет работать асинхронно более чем с одним соединением. В таких случаях стабильный и сопровождаемый синхронный результат лучше «выстраданного» асинхронного, который «страшно тронуть».
4. Alex W. Lulin
22 Ноя 2013 12:10 дп
— ошибаешься :-) когда тебе надо «проиндексировать 3 Тб документов», а «ускорять уже некуда», то можно взять тачку с 12-ю процессорами (благо они совсем не новость) и думать о параллельных вычислениях.
5. Alex W. Lulin
22 Ноя 2013 12:11 дп
— а вот тут есть сложности связанные с «непоторозащищённостью GDI». Если потоки работают с GDI, То много «разных эффектов» возникает.
Не раз нажирался.
Не говорю уж про «кеш шрифтов» от Borland’а.
6. Alex W. Lulin
22 Ноя 2013 12:13 дп
Если бы я писал что-то подобное твоему «микро-лиспу» — я бы непременно устроил бы на этой технике параллельные вычисления независимых функций. Благо все современные вычислительные системы — многопроцессорные ну или «многоядерные».
7. Alex W. Lulin
22 Ноя 2013 12:15 дп
— это — БЕЗУСЛОВНО улучшение. Качественное.
Можно «занудный вопрос»? А почему всё же Free, а не FreeAndNil?
8. Роман Янковский
22 Ноя 2013 12:27 пп
Alex W. Lulin,
:)
Ты там у меня начало фразы оторвал. Индексация 3 Тб документов на тачке с 12-ю процессорами — это как правило не то, чем занимаются фронтенд-приложения. Хотя ситуации конечно разные бывают.
Я, честно говоря, не вижу зачем там FreeAndNil. Я вообще FreeAndNil редко использую. Не потому, что в этом есть какая-то глубокая идея, в просто потому, что редко возникает такая необходимость.
9. Alex W. Lulin
22 Ноя 2013 11:17 пп
Зря :-) почитай хотя бы GunSmoker’а. Хотя я что-то сомневаюсь, что ты не читал.
10. Alex W. Lulin
22 Ноя 2013 11:19 пп
— да, есть такой мой косяк. Я уж подумал об этом.
Ну я надеюсь — ты понял, что я имею в виду. Я же не в плане «придирок» и даже не в плане «совета», а в плане «вот такие бывают северные олени, с которыми я сталкивался». ;-)
11. Alex W. Lulin
22 Ноя 2013 11:20 пп
http://www.gunsmoker.ru/2009/04/freeandnil-free.html
Если ВДРУГ не читал.
12. Всеволод Леонов
25 Ноя 2013 9:01 пп
Заставили меня на семинаре в Киеве сделать TThread на мобильнике.
Сделал! Два потока, они там что-то считали (синусы от косинусов), а параллельно без блокировки я в TChart добавлял какие-то данные в главном потоке.
Прикол, люди РЕАЛЬНО не знают, что в Delphi можно сделать File->New->Delphi other files -> ThreadObject
и она загенерит заглушку, причем с анонимкой! :) Причем именно не «нубы» какие-то, а монстры!
FreeAndNil — отстой (я полицейский значок снял и положил на капот).
Это только Сюшники могут удалять объект внешней функцией.
Ref.Free — наше всё. Проще не забыть нулить вручную, типа как «магазин застегнуть». Ну а если локально, то после finally как-то еще юзать оъект — «ну извините»!
Сюшникам-то привчно бить объект «извне», а не через точку. Но там-то пара логичная new/delete. А тут? Не, если к изяществу TMyClass.Create добавить еще манную кашу FreeAndNil — то что мы получим? Где тут балет «лебединой озеро»? Какой-то раммштайн. Хорошо, что в мобильном компиляторе мы от этого избавлены :)
Одел значок.
С точки зрения «мобильности». Тут как раз бы сварить многопоточный ДатаСнап сервер, а мобильный фронтэнд на колбэки повесить смотреть статус потоков и их синхронизацию. Можно ли синхронайз коллбэку сделать?
(умных много, смелых мало)
Господа (Люлин и Янковский), блин, вы оба — такие умные, что хватит уже придираться друг к другу. Александр, ну как можно подумать, что Роман FreeAndNil не знает? Он бравирует своей удалью (типа тормоза придумал трус). Короче, напишите совместную статью в знак примирения.
13. Alex W. Lulin
26 Ноя 2013 6:11 пп
И в мыслях не было.
14. Alex W. Lulin
26 Ноя 2013 11:21 пп
Всеволод! Если Вы думаете, что я «придираюсь» к Роману, то Вы — не правы. Просто спросил.
А насчёт FreeAndNil — Вы неправы. Но я не буду начинать этот бесконечный спор.
А что хочу сказать по сути?
Роман продемонстрировал ОФИГЕННУЮ вещь! Просто и со вкусом. СПАСИБО, Роман!
Я обязательно прикручу её к своему коду.
Я даже знаю куда.
1. Тесты иногда запускают несколько потоков, чтобы например обрабатывать асинхронно контекстное меню.
2. В индексаторе есть сортировка слиянием. Независимые пары коробок — можно сортировать асинхронно.
3. В редакторе есть рендеринг пачки параграфов. Его тоже можно делать асинхронно.
15. Александр Багель
3 Дек 2013 4:18 пп
Идея здравая, понравилось, только каждый раз дергать создание нити имхо чересчур, можно наверное что-то наподобие пула прикрутить.
Далее, к примеру результатом должен возвращаться некий объект (тот-же TObject), здесь мы имеем огромный шанс получить мемлик.
Я бы переделал немного так: при вызове метода TAwaitable.Value: T; выставлять флаг, мол объект передан наружу и в деструкторе проверять этот флаг.
Если созданный объект не потребовался, разрушить его (ну как-то так, сумбурно…)
16. a
4 Дек 2013 5:38 дп
тот же std::future из c++11
17. Alex W. Lulin
5 Дек 2013 1:39 дп
http://www.stdthread.co.uk/doc/headers/future/future.html
ВЫ ПРАВЫ. Хорошая вещь.
И?
В «контексте Delphi» как он «помогает»?
18. Alex W. Lulin
5 Дек 2013 2:38 дп
Вот это две конструкции кстати КРАЙНЕ интересны:
future& operator=(future&&);
future& operator=(future const&) = delete;
19. Ришат Бикчантаев
31 Дек 2013 3:24 пп
Недавно столкнулся (правда в D7) с проблемами организации потоков:
Допустим я внутри WaitFor2 хочу указать метод класса принадлежащего форме.
Получим ошибку моле не проведена инициализация, в принципе обходится:
Но тут уже поймаем другую ошибку при убивании формы EOSError Code: 1440 Неверный дескриптор окна
Как быть в таких случаях?
20. balmo
1 Мар 2014 8:55 пп
Сев, по поводу «Ref.Free — наше всё»
Предлагаю вообще в последующих версиях выпилить FreeAndNil (раз уж это отстой), а может до кучи и Free. Destroy рулит!
А ещё можно не пристегиваться в машине, ездить на мотоцикле без шлема, не пользоваться другими иными средствами м… предохранения от последствий.
Фу, Всеволод! Ты же препод! Вот как можно советовать в публичном блоге отказываться от практик, которые новичкам только на пользу? Понятно, что это оправданно не всегда. Когда дорастут до COM и прочих плюшек с расширенным управлением памятью — сами поймут когда FreeAndNil а когда Free. А так от твоих фраз ведь правда поверят же что «FreeAndNil — отстой». Не уподобляйся, пожалуйста, некоторым профи типа Ника Х., которые не учитывают различный уровень читающих. Ты же публичная фигура.
ИМХО: не пристегиваться в авто — это не крутость, а глупость. Сказочная.
21. Сергей Андреевич
5 Мар 2014 8:27 пп
Допустим, я выполняю длительную операцию в цикле, и мне надо неожиданно остановить выполнение фоновой задачи. Сейчас, нужно делать глобальную переменную для остановки, и проверять в потоке прервали или нет.