суббота, 7 сентября 2013 г.

await добрался до C++

Смотрю GoingNative 2013.

Особенно интересно выступление по теме "Bringing await to C++". "Асинронность" (ожидаемо) идет в массы и становится трендом. Настолько, что ее поддержку внедряют в компилятор и в будущий стандарт C++ (надеюсь, что последнее все-таки свершится). Понятное дело, это все для того, чтобы сделать программирование "асинхронности" максимально удобным для обывателей "среднестатистического программиста" и лучше оптимизировать.

Stackfull coroutines (наконец-то) идут в массы. Всего какой-то год назад "текли слюни" при взгляде на поддержку await в C#! Видимо, компании, которые массово используют C++ и потому влияют на его развитие, действительно активно используют асинхронные алгоритмы и остро нуждаются в средствах, способных облегчить их реализацию на C++ (а заодно и лучше оптимизировать подобный код).

Кто еще сомневается в необходимости работать с вводом-выводом асинхронно даже в клиентской части (например, уеt another messenger)? На "рельсы асинхронности" уже планируют перевести не только сетевой ввод-вывод - смотрите, примеры-то у всех (кто говорит, про PPL, std::future::then и даже про уже существующий WinRT) работают со "старыми добрыми" файлами (а, может быть, просто у них пока нет чего-то более-менее стандартного и краткого для сетевого ввода вывода?). И не только (правда, там уже делают больший упор на lazy-вычисления).

P.S. Q&A Panel особенно порадовала. Определенно, нынешний GoingNative гораздо лучше предыдущего и лучше уже прошедшего C++Now. Отличные докладчики (хотя их состав почти не меняется) с неплохим чувством юмора!

10 комментариев:

Анонимный комментирует...

Для C++ await будет катастрофой. Именно потому, что это не stackfull. По-настоящему stackfull - это boost.coroutine. А также разрабатываемая всё тем же Оливером boost.fiber.
И вообще, этот пример наглядно показывает, что await в ядре языка не нужен, если предложение Оливера всё-таки предпочтут разрушительной попытке мелкомягких во главе с Саттером уподобить плюсы недошарпу.

Анонимный комментирует...

Ну, и если непонятно: async/await - это всё та же колбэчная лапша. Только теперь неявная. В случае C++ это означает очередные детские грабельки в области управления памятью.
Исключения, в отличие от fiber'ов, летают совсем не по семантике языка, а "как шмагла" - они оголяют все кишки лапши а-ля node.js, и хотя компилятор и будет "заботливо" перекручивать код, семантику колбэков всё равно необходимо будет учитывать для обработки исключений, что сводит на нет весь профит... которого и так нет.

Марат Абраров комментирует...

Именно потому, что это не stackfull. По-настоящему stackfull - это boost.coroutine. А также разрабатываемая всё тем же Оливером boost.fiber.
Я чего-то не понимаю? Мне видится C++ await и resumable как boost.coroutine средствами компилятора. Разве не так?

Марат Абраров комментирует...

Исключения, в отличие от fiber'ов, летают совсем не по семантике языка, а "как шмагла" - они оголяют все кишки лапши а-ля node.js, и хотя компилятор и будет "заботливо" перекручивать код, семантику колбэков всё равно необходимо будет учитывать для обработки исключений, что сводит на нет весь профит... которого и так нет.

Думается мне, что в случае обратных вызовов с std::future::then, с исключениями C++ все в пределах нормы (за счет std::exception_ptr "со товарищи").

А в C++ await за счет "side stack" все даже проще. Более того, "side stack" ка раз таки и подразумевает отсутствие скрытой "колбэчной лапшы". Кажется, в докладе и проскакивало, что C++ await и работает за счет fiber-ов...

Анонимный комментирует...

Я чего-то не понимаю? Мне видится C++ await и resumable как boost.coroutine средствами компилятора. Разве не так?

Ok, можно воспринимать и так. Тогда вопрос: а зачем средства компилятора и новые ключевые слова, если можно просто библиотекой? Мелким и мягким они нужны, чтобы поддержать всю асинхронность WinRT в их очередном C++, да ещё и сделать это как в шарпе. А зачем это всему сообществу C++ разработчиков? Я ведь уже привёл вам пример реализации await на boost.coroutine.
Потом, почему я не считаю это по-настоящему stackfull: код должен быть готов работать с resumable функциями. И если у меня уже есть некий парсер

xml_document parse(iterator begin, iterator end);

то я никак не смогу на resumable сконстрячить итератор, асинхронно получающий данные из сети - сам код парсера должен быть готов к такому повороту. А вот с настоящими stackfull сопрограммами из boost я легко сделаю такой итератор - просто буду вызывать parse из сопрограммы, а итератору отдам coro_t::caller_type, чтобы делать переключение контекста. Очевидно, что с await/resumable это невозможно, ибо await должен быть именно в resumable и действует только на него. Я не могу в инкременте итератора сказать "сделай await той resumable, которая вызвала parser". Я должен буду протащить await и resumable по всему коду парсера, как монаду по хаскелю.
Вот на таком простом примере и оголяется вся колбэчная семантика await. Именно семантика, а не как оно там реализовано.

И вообще, разговор то о boost.fiber. Ибо именно их я считаю решением проблемы колбэчной лапши в асинхронности. Я говорю о возможности делать по green thread'у на обслуживаемого клиента. О подходе Erlang или, что ближе к boost.fiber, о подходе Go - эти вещи проверены временем, в отличие от.

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

Думается мне, что в случае обратных вызовов с std::future::then, с исключениями C++ все в пределах нормы (за счет std::exception_ptr "со товарищи")


С микротредами мы используем исключения как привыкли, предлагаемый же костыль только говорит нам, что где-то есть некий task. Сам этот факт ломает семантику языка. А язык у нас линейный - так людям легче понимать. Поток управления должен быть последовательным, а не "вот тут мы родили task'у, а исключение из неё получили воооооон там". Это трэш, угар и содомия. С микротредами всё линейно и последовательно с асинхронным back-end'ом, так зачем мне эта мелкомягкая порнография?

Марат Абраров комментирует...

Тогда вопрос: а зачем средства компилятора и новые ключевые слова, если можно просто библиотекой? Мелким и мягким они нужны, чтобы поддержать всю асинхронность WinRT в их очередном C++, да ещё и сделать это как в шарпе. А зачем это всему сообществу C++ разработчиков? Я ведь уже привёл вам пример реализации await на boost.coroutine.

Против boost.coroutine ничего не имею и всячески рад его наличию. Только оно работает без каких-либо гарантий того, что не сломается в новой версии компилятора или при добавлении новых регистров процессора, потому что компилятор не дает (библиотечных) средств для реализации stackfull.
C++ await, кстати, отличается от C# await за счет создания копии стека, а не игр с callback/continuation на базе C# Iterator.

Потом, почему я не считаю это по-настоящему stackfull: код должен быть готов работать с resumable функциями.

Годное замечание. С этим следует что-то придумать (ввести возможность явного использования контекста и, соответственно, его передачи - с boost.coroutine вы же то же самое делать будете в этом случае).

Повторюсь: C++ await, по сути есть тот же (должен быть тем же, что и) boost.coroutine, но реализованная средствами компилятора. За счет этого должны добавиться простота/лаконичность, дополнительные возможности для оптимизаций, (надеемся что качественная) поддержка производителем компилятора C++, а не сторонними разработчиками, стандартизация и соответствующее проникновение в код проектов на C++.

Наверное, действительно, лучше было бы иметь 2 механизма (второй, построен на базе первого):
1. std::coroutine,
2. C++ await.

Марат Абраров комментирует...

И вообще, разговор то о boost.fiber. Ибо именно их я считаю решением проблемы колбэчной лапши в асинхронности. Я говорю о возможности делать по green thread'у на обслуживаемого клиента. О подходе Erlang или, что ближе к boost.fiber, о подходе Go - эти вещи проверены временем, в отличие от.

Fibers есть user-mode/user-scheduled threads. Тут есть нюансы. Например, размер стеков. Но самое главное это то, что fibers опять возвращают нас к коду а-ля "на каждую длительную/долгоживущую задачу свой поток" и опять заставляют ломать голову над (безопасными) инфообменом/синхронизацией м/у этими задачами.

У Erlang не fibers, а акторы. У Go, кажется, еще есть channels. По одному "green thread'у на обслуживаемого клиента" не получится, потому что обслуживание клиента, в общем случае, это (динамического размера) набор нескольких таких green thread-ов, где один отвечает за чтение, другой - за запись, третий - за таймер активности/неактивности, четвертый - за выполнение задачи обслуживания #1, пятый - за выполнение задачи обслуживания #2 и т.д. При чем эти green thread-ы то spawn-ятся, то combine-яться. Fibers, IMHO, решают лишь проблему с кол-вом потоков, но не решают проблемы безопасного и удобного параллельного программирования многоядерных/многопроцессорных (SMP/NUMA) систем.

С микротредами мы используем исключения как привыкли, предлагаемый же костыль только говорит нам, что где-то есть некий task. Сам этот факт ломает семантику языка. А язык у нас линейный - так людям легче понимать. Поток управления должен быть последовательным, а не "вот тут мы родили task'у, а исключение из неё получили воооооон там". Это трэш, угар и содомия.

Это у человека сознание однопоточное, а в физическом мире все есть акторы состоящие из акторов, состоящих из акторов... Думается, попытки переложить их на линейную логику (в конце концов, рано или поздно) обречены. Сейчас во всю идут "конвульсии" императивных языков и усиление функциональной парадигмы.

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

Однозначно "да". Но в докладе было про то, что C++ await не годится для тех схем "массового обслуживания", для которых отлично работает Asio. У Криса, кстати, есть еще и вкусный "Asynchronous Fork/Join using Asio" (http://blog.think-async.com/2008/10/asynchronous-forkjoin-using-asio.html).

Марат Абраров комментирует...

Добавлю еще.
Лично я, в общем случае ("по дефолту") введению новых элементов языка предпочитаю реализацию нужного функционала за счет создания библиотечных средств.
Но, давайте, возьмем виртуальные функции-члены (методы) и попробуем без лишней лапши и так же эффективно реализовать их на C. Так же эффективно! Я, например, видел аналоги виртуальных методов в C-шном ANTLR и, полагаю, где-нибудь в GObject оно сделано точно так же. Таблица виртуальных методов, создаваемая VC++ - это не таблица с адресами функций. Да, это завязано на формат образа исполняемого файла и пр. "специфичности Windows". А если еще взять такой вид оптимизации как "девиртуализация", то становится понятным, что иногда реализация на уровне языка/компилятора может выигрывать по эффективности у библиотек/фреймворков.

Анонимный комментирует...

Против boost.coroutine ничего не имею и всячески рад его наличию. Только оно работает без каких-либо гарантий того, что не сломается в новой версии компилятора или при добавлении новых регистров процессора, потому что компилятор не дает (библиотечных) средств для реализации stackfull.

Повторюсь: C++ await, по сути есть тот же (должен быть тем же, что и) boost.coroutine, но реализованная средствами компилятора. За счет этого должны добавиться простота/лаконичность, дополнительные возможности для оптимизаций, (надеемся что качественная) поддержка производителем компилятора C++, а не сторонними разработчиками, стандартизация и соответствующее проникновение в код проектов на C++.

Лично я, в общем случае ("по дефолту") введению новых элементов языка предпочитаю реализацию нужного функционала за счет создания библиотечных средств. <...> иногда реализация на уровне языка/компилятора может выигрывать по эффективности у библиотек/фреймворков.

Так я же вам в первом сообщении дал ссылку на предложение комитету от Оливера! Или по-вашему поддержка компилятором - это обязательно новые ключевые слова??? Реализация C++ может реализовать std::coroutine так же эффективно, как и await. И даже эффективнее: раскройте секрет, как вы будете задавать аллокатор side stack'а для resumable? Или вы опять начнёте думать над этой проблемой, как над проблемой переписывания кода под await? Зачем всё это нужно, если уже есть эффективное решение?

Наверное, действительно, лучше было бы иметь 2 механизма (второй, построен на базе первого):
1. std::coroutine,
2. C++ await.


Это уже вообще перл. Зачем второе? Оно менее эффективное и не поддерживает имеющийся stream-ориентированный код.

Анонимный комментирует...

C++ await, кстати, отличается от C# await за счет создания копии стека, а не игр с callback/continuation на базе C# Iterator.

Ничего подобного. Это так реализовали те, кто показывал на GoingNative. В предложении комитету это лишь одна из возможных реализаций. Есть и с перекручиванием кода как в недошарпе. Одно это говорит об их семантическом единстве. Но это так, к слову...

Fibers есть user-mode/user-scheduled threads. Тут есть нюансы. Например, размер стеков.

Надо понимать, что side stack выделяется в астральной куче? А между тем gcc и gold linker поддерживают рост стека по запросу связным списком. И Оливер поддержал эту возможность для аллокаторов, Go тоже использует эти плюшки. И только M$ тратит время на очередной C++ и генерацию бредовых предложений комитету, вместо доведения поддержки C++11 до уровня gcc/clang и стека по запросу. Так держать!

Fibers есть user-mode/user-scheduled threads. Тут есть нюансы. Например, размер стеков. Но самое главное это то, что fibers опять возвращают нас к коду а-ля "на каждую длительную/долгоживущую задачу свой поток" и опять заставляют ломать голову над (безопасными) инфообменом/синхронизацией м/у этими задачами.

По одному "green thread'у на обслуживаемого клиента" не получится, потому что обслуживание клиента, в общем случае, это (динамического размера) набор нескольких таких green thread-ов, где один отвечает за чтение, другой - за запись, третий - за таймер активности/неактивности, четвертый - за выполнение задачи обслуживания #1, пятый - за выполнение задачи обслуживания #2 и т.д. При чем эти green thread-ы то spawn-ятся, то combine-яться.

Зачем? Поставь на таймер обычный обработчик, используйте асинхронный функции для записи/чтения как в примерах boost.fiber. А по поводу "задач обслуживания" - если это полнодуплексный протокол, то вам придётся заводить ещё один конечный автомат обработчиков или стартовать resumable - те же яйца, только в профиль.

Fibers, IMHO, решают лишь проблему с кол-вом потоков, но не решают проблемы безопасного и удобного параллельного программирования многоядерных/многопроцессорных (SMP/NUMA) систем

А эту проблему никто не решает.

У Erlang не fibers, а акторы.

Да, акторы. Как это противоречит тому, что эрланговские процессы - суть green threads?

У Go, кажется, еще есть channels.

Да, каналы, их аналог в boost.fibers - очереди. Как это противоречит тому, что go-routine - суть green threads?

Это у человека сознание однопоточное, а в физическом мире все есть акторы состоящие из акторов, состоящих из акторов...

Я вам секрет открою: языки программирования и создаются для человека.

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

Я бы на вашем месте в экстренном порядке сообщил Армстронгу и Пайку, что их языки обречены.