поток мысли о современном корутинном программировании
Тут пришла в голову мысль, что известные решения проблеммы спагеттизации асинхронного кода (написанного с callbacks/continuations/futures) приносят некоторые дополнительные изначально не очевидные проблемы.
Сценарий - коммуникация с сервером. В исходном варианте (со спагетти), имеем что-то вроде
SendRequest(request, processReplyFunction);
где processReplyFunction - функция, которая будет вызывана при получении ответа. Ну, там, это может быть не просто функция, а объект класса у которого вызовется виртуальная функция, или лямбда, всё что угодно, что можно вызывать и передать Reply - не суть.
И когда код превращается в череду вызовов таких колбеков - поддерживать его выходит сложновато.
Решение данной проблемы с помощью монад в немонадном языке типа С++ даже обсуждать не стоит. Чисто академическое упражнение (интересное), для реальной работы не годится.
Корутины С++ - это да, это решение.
Reply reply = co_await SendRequest(request);
// process reply
Если что, вызов корутины на С#, javascript, python выглядит так же.
А ещё, казалось бы, лучше без всякого co_await, просто
Reply reply = SendRequest(request);
// process reply
где вся корутинная работа делается вообще без изменения декларации функции и без изменения синтаксиса вызова. Уж не знаю, есть ли язык, где такое поддерживается.
Теперь, проблема. Если выполнение корутины прерывается (скажем, мы послали запрос, остановились для ожидания ответа), то могут возникать ситуации "конкурентного" доступа к данным. То есть, даже если у нас есть в программе один единственный тред, на него всё же скедулятся те или иные куски корутин, перемежающиеся между собой в заранее тяжело предсказуемой последовательности. И... мы немедленно попадаем в мультитредный мод программирования, пусть этот "мультитред" и без переключения контекста по таймеру. И теперь - чем меньше заметен корутинный вызов - тем более у нас шансов наступить на эти грабельки. И, хуже всего, превращая какую-то уже давно написанную функцию в асинхронную (была функция обычная, а потом в неё пришлось добавить запрос к базе данных), мы немедленно создаём вышеуказанную проблему для тех, кто её вызывает, поскольку каждая точка такого вызова - это потенциальный race condition. И в общем, даже ключевое слово co_await не сильно и спасает.
А теперь вернёмся к прежднему старомодному спагетти-стилю и обнаружим, что там точки, где может случиться "переключение контекста" абсолютно четко выделены. )))
Тут где-то рядом (у меня в голове) находится вопрос - а стоит ли писать мультитредный thread-pool, на которых мы, разумеется, немедленно захотим гонять те же корутины? Очевидно, если мы можем разнести логику так, чтобы отдельные Jobs не обращались к общим данным, то мультитред нам не помеха. Не помеха он и там, где таких данных очень мало и стратегию из защиты можно легко определить и локализовать в небольших кусках кода. Иначе... иначе мы возвращаемся к старому доброму мультитредному программированию со всеми его проблемами, плюс, кстати, нам надо переимлементировать с нуля всю библиотеку синхронизации так, чтобы она позволяла переключать работу от заблокированной корутины на другую корутину, ожидающую своей очереди. Пожалуй, тут нет ничего более страшного, чем обычный мультитред без корутин. Если же у нас есть только один тред, на который "скедулятся" корутины, то см. выше, повторю, что при излишней лёгкости написания такого кода мы налетаем на проблемы отслеживания конкурентного доступа, что в самом худшем случае эквивалентно написанию кода мультитредного, а в случае среднем нам придётся тщательно выслеживать, как сочетается доступ данных с точками возможного переключения контекста, будь то спагетти с колбеками, где такие точки очевидны, или же co_await, где такие точки очевидны куда менее.
Сценарий - коммуникация с сервером. В исходном варианте (со спагетти), имеем что-то вроде
SendRequest(request, processReplyFunction);
где processReplyFunction - функция, которая будет вызывана при получении ответа. Ну, там, это может быть не просто функция, а объект класса у которого вызовется виртуальная функция, или лямбда, всё что угодно, что можно вызывать и передать Reply - не суть.
И когда код превращается в череду вызовов таких колбеков - поддерживать его выходит сложновато.
Решение данной проблемы с помощью монад в немонадном языке типа С++ даже обсуждать не стоит. Чисто академическое упражнение (интересное), для реальной работы не годится.
Корутины С++ - это да, это решение.
Reply reply = co_await SendRequest(request);
// process reply
Если что, вызов корутины на С#, javascript, python выглядит так же.
А ещё, казалось бы, лучше без всякого co_await, просто
Reply reply = SendRequest(request);
// process reply
где вся корутинная работа делается вообще без изменения декларации функции и без изменения синтаксиса вызова. Уж не знаю, есть ли язык, где такое поддерживается.
Теперь, проблема. Если выполнение корутины прерывается (скажем, мы послали запрос, остановились для ожидания ответа), то могут возникать ситуации "конкурентного" доступа к данным. То есть, даже если у нас есть в программе один единственный тред, на него всё же скедулятся те или иные куски корутин, перемежающиеся между собой в заранее тяжело предсказуемой последовательности. И... мы немедленно попадаем в мультитредный мод программирования, пусть этот "мультитред" и без переключения контекста по таймеру. И теперь - чем меньше заметен корутинный вызов - тем более у нас шансов наступить на эти грабельки. И, хуже всего, превращая какую-то уже давно написанную функцию в асинхронную (была функция обычная, а потом в неё пришлось добавить запрос к базе данных), мы немедленно создаём вышеуказанную проблему для тех, кто её вызывает, поскольку каждая точка такого вызова - это потенциальный race condition. И в общем, даже ключевое слово co_await не сильно и спасает.
А теперь вернёмся к прежднему старомодному спагетти-стилю и обнаружим, что там точки, где может случиться "переключение контекста" абсолютно четко выделены. )))
Тут где-то рядом (у меня в голове) находится вопрос - а стоит ли писать мультитредный thread-pool, на которых мы, разумеется, немедленно захотим гонять те же корутины? Очевидно, если мы можем разнести логику так, чтобы отдельные Jobs не обращались к общим данным, то мультитред нам не помеха. Не помеха он и там, где таких данных очень мало и стратегию из защиты можно легко определить и локализовать в небольших кусках кода. Иначе... иначе мы возвращаемся к старому доброму мультитредному программированию со всеми его проблемами, плюс, кстати, нам надо переимлементировать с нуля всю библиотеку синхронизации так, чтобы она позволяла переключать работу от заблокированной корутины на другую корутину, ожидающую своей очереди. Пожалуй, тут нет ничего более страшного, чем обычный мультитред без корутин. Если же у нас есть только один тред, на который "скедулятся" корутины, то см. выше, повторю, что при излишней лёгкости написания такого кода мы налетаем на проблемы отслеживания конкурентного доступа, что в самом худшем случае эквивалентно написанию кода мультитредного, а в случае среднем нам придётся тщательно выслеживать, как сочетается доступ данных с точками возможного переключения контекста, будь то спагетти с колбеками, где такие точки очевидны, или же co_await, где такие точки очевидны куда менее.
no subject
Хорошо бы, чтоб читабельность. А так-то да.
Я уже второй день решаю похожий вопрос - вполне съедобный код с колбаками надо поменять, добавив еще операция, которая сейчас делается в вызывающем коде, а надо теперь в вызываемом (там данные).
Тьфу, вот не придумать солюшен. С монадой было бы проще. И это Скала.