четверг, 15 января 2015 г.

[prog.c++.thoughts] Продолжение размышлений на тему короутин в C++ приложениях

Очередная заметка в серии про короутины и await с попыткой понять, к чему все это идет. Первая заметка серии здесь, вторая здесь. Так же читателям может быть интересна более ранняя заметка "FiniteStateMachines vs Coroutines".

Сразу предупреждаю, что изложенное ниже -- это отнюдь не истина в последней инстанции, это даже не мое текущее мнение по этому поводу. А просто опубликованная запись некоторых мыслей на эту тему.

Итак, в каком случае хороши короутины? В случае, когда нужно выстроить линейную цепочку внешних вызовов, как-то комбинируя результаты этих вызовов между собой (например, передавая результат предыдущего вызова в параметром следующего вызова). Т.е. действия вида:

void typical_coroutine()
{
   auto d = prepare_data(); // Can take some time.
   auto r = process_data(d); // Can take yet more time.
   store_data(r); // Can initiate a lot of sync/async IO operations.
}

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

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

Но ведь для выражения таких цепочек действий у разработчиков уже есть инструмент. А именно -- нити (они же потоки, они threads). Так зачем кроме нитей нужны еще и короутины?

Как мне кажется, на поверхности лежат две простые и очевидные причины. Хотя, если присмотреться, там все не так уж просто и очевидно.

Производительность.

Переключение нитей -- это довольно тяжелая операция, осуществляющаяся в ядре ОС, тогда как переключение короутин представляется более легкой операцией, т.к. может выполняться без дергания функций ОС, как говорят, в user space.

На самом деле хорошая причина. Весьма веская. Вопрос только в том, что на современной вычислительной технике, где доступ к данным в RAM, по сравнению с доступом к данным в кэше процессора, очень медленный, для достижения высокой производительности нужно вообще избегать переключений контекстов. В идеале, код должен быть линейным, выполняющимся в одном потоке, по минимуму обменивающимся данными с другими ядрами процессора (или другими процессорами).

Выгода от короутин появляется тогда, короутина является чем-то вроде диспетчера внешних, по отношению к ней действий (как в примере выше -- prepare_data, process_data и store_data -- это какие-то тяжелые операции, которые непонятно как и где обрабатываются). Какие накладные расходы несут в себе эти действия? Нет ли в них самих десятков/сотен переключения контекстов, обмена данными с другими ядрами, перемещения больших объемов данных между RAM и кэшами процессора? И не убьет ли все это тот выигрыш, который дает коуротина по сравнению с потоком?

Масштабируемость.

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

Однако, тут нужно задуматься, а почему ОС с трудом переваривают десятки тысяч потоков? И сколько труда было вложено далеко не самыми плохими программистами в диспетчеры ОС с тем, чтобы ОС могла эффективно диспетчировать тысячи потоков, обеспечивая при этом более-менее приемлемую отзывчивость? Сколько всяких трюков и оптимизаций лежит, например, за такой простой операцией, как освобождение mutex-а?

И теперь представим, что все это нужно будет получить внутри приложения, порождающего внутри себя сотни тысяч короутин. Которым нужно как-то синхронизироваться, уступать друг другу, ждать друг друга, будить друг друга. Это что, означает необходимость ваять и встраивать внутрь приложения аналог диспетчера ОС?

Ну и, кроме того, если короутины рассматривать как очень легкие и дешевые альтернативы нитям, то не получится ли, что в итоге программирование с короутинами приведет к тем же проблемам, что и программирование с нитями: гонки, дедлоки, непонимание того, где, что и в какой момент сработает, кто когда не там обгонит и кто и где кого-то будет слишком долго ждать?

Комментариев нет: