Как стать автором
Обновить

Комментарии 58

Закрепленные Закреплённые комментарии
Замечательная статья!
В защиту автора (хотя, мне кажется, он не нуждается в защите) выражу свое мнение: подход, основанный на истинных stackful сопрограммах, который на протяжении уже нескольких лет разрабатывает автор, мне кажется очень перспективным. И намного более глубоким и красивым, чем то, что предлагают ввести в стандарт C++. Стандарты всегда запаздывают, так как описывают вещи для всех уже привычные и тысячу раз испробованные.
Недостатками этого подхода мне видится две вещи:
1. Отдельный стек для каждой сопрограммы. На самом деле, это достоинство (все данные — на стеке, всегда под рукой, сколько бы нас не прерывали), но за которым надо внимательно следить, иначе при чрезмерном увлечении можно всю память сожрать. Быть может, стоит ограничить размер стека сопрограмм, но это уже детали.
2. Необычность. Этот «недостаток» лечится только опытом. Во-первых, надо попробовать все другие методы асинхронного/многопоточного программирования, начиная от тривиальных mutex/condvar, наплодить 100500 потоков на 4-ядерной машинке, удивиться «а чё оно так медленно работает и падает/лочится», затем пройти через callback hell, попробовать переписать callback на stackless сопрограммах, понять, что просто это не удастся, надо много переписывать. Во-вторых, надо научиться думать в терминах модели stackful сопрограмм; это бывает весьма нелегко, — заставить свой мозг работать по-другому, в терминах новой модели. Например, синхронизировать вовсе без примитивов синхронизации. Или: сделать многопоточную программу, в которой многопоточная логика не вылезает наверх, не затеняет логику самой программы, так что, читая код, вы видите, что делает программа, а не потроха взаимодействия потоков, future, callback и прочую машинерию, за которой леса не видно.

Вообще, трудность введения новых моделей взаимодействия компонентов программы всегда одна и та же: программу приходится переписывать практически с нуля. И вторая трудность — выбор правильной модели.

PS: сам я модель stackful сопрограмм не применял по причинам, описанным выше, — негде было. Но искренне завидую тем, кто это попробовал на реальных задачах.
Всё это очень умно, но связи с реальностью в этой статье очень слабые. Здесь пропущен важный пункт:

Микропроцессору всё равно сколько абстракций накручено над задачей, он выполняет голый машинный код.

В связи с этим, чем лучше выбранный язык программирования может описать исходную задачу, без всяких абстракций и накруток, тем лучше решение. Если кому-то потребуется написать сложное асинхронное приложение, то вряд-ли он станет разбираться в подобных абстракциях, а скорее возьмет Go или node.js, которые автоматически конвертируют исходный код в 'сопрограммы'.

Я подозреваю статья родилась следующим образом: человек пришел в Яндекс, поглядел, удивился, «вау, как тут всё круто!» Нужно не отставать от уровня! И без опыта, полученного потом и кровью, решил: дай-ка напишу какую-нибудь заумную статью, чтобы все сразу сказали «вау!»
Микропроцессору всё равно сколько абстракций накручено над задачей, он выполняет голый машинный код.

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


Если кому-то потребуется написать сложное асинхронное приложение, то вряд-ли он станет разбираться в подобных абстракциях, а скорее возьмет Go или node.js, которые автоматически конвертируют исходный код в 'сопрограммы'.

Код не может автоматически конвертироваться в сопрограммы. Он должен быть написан с учетом текущей модели исполнения. Если это сопрограммы, то будут использоваться преимущество сопорграмм.


Я подозреваю статья родилась следующим образом: человек пришел в Яндекс, поглядел, удивился, «вау, как тут всё круто!»

Вы просто не представляете, как сильно вы ошибаетесь. Причем везде.


Статья родилась как результат анализа и обобщения различных подходов, кодовых баз и опыта. А заниматься шапкозакидательством — много ума не надо.

Только почему-то оказывается, что голому машинному коду не все равно на количество абстракций. И, как показывает практика, чем больше абстракций, тем тормознее код. Часто для производительности приходится их выпиливать.
Если что, Вы только что опубликовали статью про то как увеличить количество абстракций. Лично мне не хватило когнитивного ресурса их всех понять, а главное понять зачем они.
Код не может автоматически конвертироваться в сопрограммы.
Разумеется, если этого не знать, то можно написать целый цикл статей про то, как попытаться состряпать это на C++. Но, например, в JS есть generator functions.
Статья родилась как результат анализа и обобщения различных подходов, кодовых баз и опыта.
И Путин её одобрил, как я понимаю?
Лично мне не хватило когнитивного ресурса их всех понять, а главное понять зачем они.

Писать проще, чем читать, я понимаю.


Разумеется, если этого не знать, то можно написать целый цикл статей про то, как попытаться состряпать это на C++. Но, например, в JS есть generator functions.

А в Хаскеле есть монады!


И Путин её одобрил, как я понимаю?

Без одобрения Путина данная статья, безусловно, не появилась бы на свет.

НЛО прилетело и опубликовало эту надпись здесь

Надо подумать, как это лучше всего сделать. Это непростой вопрос, т.к., фактически, он затрагивает аспекты дизайна и проектирования систем вообще. Как правило, это достигается на опыте написанием своего прототипа или модуля с нуля. Хотя есть и толковые книжки, которые разъясняют это подробно.


Здесь же мы имеем дело со взаимодействием объектов в многопоточной среде. Возникающие проблемы осознаются только тогда, когда с ними непосредственно сталкиваешься. Соответственно, статья как раз о том, как справляться с проблемами взаимодействия объектов.


Тут можно посоветовать лишь брать и пробовать. Но я подумаю о том, чтобы описать стандартные паттерны и подходы к такому снаряду.

НЛО прилетело и опубликовало эту надпись здесь
Ваша цель понятна. И ваши кубики в этой статье на мой взгляд будут понятны 1 из 10, или 10 из 100 сетевиков. Но если уж браться то охватывать все и сразу. А именно стать известным наставником в сетевом программировании и архитектуре. :) Раз уж вы в этом направлении двигаетесь. Таких кстати в рунете пока не видно.

Сейчас непонятно, в какую сторону я движусь. Я написал то, что хотел написать еще примерно 2 года назад. К текущему моменту накопилось множество интересных моментов, поэтому статья и получилась такой.


У вас в предыдущих докладах и статьях рассматривается пример прозрачного проксика. Т.е. создаем аксептор. Крутим цикл прочитал с сокета-записал в сокет. Дальше это все переводим в корутины с объяснениями итд.
К сожалению на практике это все не так просто.

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


Вот смотрите. В самом простом приближении, любая архитектура сетевых приложений выглядит из трех кубиков. Первый «Принимаем в буфер». Второй «Обрабатываем буфер». Третий «Отправляем с буфера». Это серверные приложения. И клиентские почти те же три кубика. Только вместо отправки из буфера, данные забираются дальше до программной логики.

Это сильно упрощенное понимание процесса. Во второй статье, кстати, приведен пример решения реальной задачи. Стоит отталкиваться от нее.


В следующий раз больше подумаю про введение в проблематику на конкретных примерах.

Только почему-то оказывается, что голому машинному коду не все равно на количество абстракций. И, как показывает практика, чем больше абстракций, тем тормознее код. Часто для производительности приходится их выпиливать.
Если что, Вы только что опубликовали статью про то как увеличить количество абстракций. Лично мне не хватило когнитивного ресурса их всех понять, а главное понять зачем они.
Код не может автоматически конвертироваться в сопрограммы.
Разумеется, если этого не знать, то можно написать целый цикл статей про то, как попытаться состряпать это на C++. Но, например, в JS есть generator functions.
Статья родилась как результат анализа и обобщения различных подходов, кодовых баз и опыта.
И Путин её одобрил, как я понимаю?

Ошиблись с сайтом. Бывает.

The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

Почему вы считаете что ваш пример с counter.set(counter.get()+1) не подходит под это определение?


counter.set(counter.get()+1) — это и есть non-atomic potentially concurrent action. Как только в программе появляется второй вызов этой же строчки без отношения happens before между ними — появляется гонка в соответствии с этим определением.

Потому что это не приводит к undefined behavior. Если запустить 2 операции параллельно, то возможно лишь 2 исхода:


  1. Обе выполнятся.
  2. Одна перетрет действие другой.

Других исходов нет.


Можно подойти с другой стороны. Внутри каждой конструкции присутствует мьютекс, который реализует отношение happens-before при обращении к общей памяти. А значит, никакого data race с точки зрения модели памяти там нет.

Я тоже не очень понимаю о чем статья/труд.
Можно как-то обозначить проблему которую решает автор? Или это просто полуобеденная дрема? :)
Я вижу что было создано семейство классов-помощников, которые что-то делают, но для чего и чем они лучше существующих API из статьи абсолютно непонятно. Истории про электроны и цитаты комьютерных деятелей это конечно замечательно, но с таким же успехом можно было бы привести цитату Степанова о том что ООП — миф и на этом оборвать всю серию размышлений…

Если говорить о практических вещах, то для комуникации сейчас есть такие популярные модели как boost::asio, node.js, ZeroMQ, и много других, каждая из них предлагает решение каких-то проблем и асинхронность. Для паралельных вычислений опять же сложность не в примитивах синхронизации, а в эффективном разбитии задачи/алгоритма на потоки, и есть решения которые распаралеливают код автоматически (у Intel и что то в GCC). Если мы говорим о низко-уровневых примитивах синхронизации (обертках над примитивами OS) — то и Mutex/SpinLock уже тоже немного устарели в угоду condition variables… Опять же в C++ новом стандарте есть опять же свои обертки (std::future). Зачем выдуманы эти все классы?
Именно. У меня создалось впечатление, что чувак создал велосипед и пиарится из-за незнания этих технологий.

ReadAdapter — это Promise или future. А все остальные пассажи — это попытка создать Promise, который масштабируется по нескольким потокам (телепортация) с автоматической синхронизацией (субъектор).

Так бы и написал «Библиотека промисов с автоматическим распределением выполнения по потокам и синхронизацией», а то «телепортация», «субъектор»…
ReadAdapter — это Promise или future.

Так promise или future?


А все остальные пассажи — это попытка создать Promise, который масштабируется по нескольким потокам (телепортация) с автоматической синхронизацией (субъектор).

Если внимательно прочитать, то окажется, что promise и future нигде не создаются. И что значит "масштабируется по нескольким потокам"? Не могли бы раскрыть этот интересный термин?

Смотри, если хорошенько задуматься, то проблемы написать асинхронное сетевое приложение особой нет. И дело не в том, где promise, а где future.

Хочешь высокопроизводительное приложение — пиши на C, C++, там будут коллбаки. Ну и что? Хочешь сложное приложение со сложной логикой — пиши на node.js, там есть и промисы, и коллбаки, и корутины. Потребление процессора в эквивалентном приложении будет выше из-за частично интерпретируемого кода, а так же будет сложно балансировать нагрузку между отдельными процессами если у тебя их несколько. Ну не беда, используй Docker и переплати немного за хостинг. Хочешь высокопроизводительное и сложное приложение — пиши на Go. Ещё есть Erlang, Java, Scala, Python со своими решениями.

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

Теперь доходит?

По-сути, ты пытаешься руками решать проблемы, которые может решать компилятор.

И возникает вопрос: а зачем?

Возможно потому что Go придуман мировой гиперкоропрацией, подчиненной тайному обществу рептилоидов-иллюминатов, а C++ — нет? Возможно, но в таком случае твоя попытка переплюнуть мировую гиперкорпорацию абстракциями на абстракциях весьма слаба.
вы предлагаете либо разводить callback hell либо тратить 90% ресурсов ПК на те же самые абстракции которые с++ предлагает по аскетично минимальной цене. Может быть, вам стоит пересмотреть взгляд на мир?
Это Вам стоит пересмотреть взгляд на мир: Go предлагает и отсутсвие callback hell и высокую производительность :D

Нет, ReadAdapter не является ни Promise ни future. Хотя бы потому что Promise/future — это паттерн безстековой асинхронности, а тут рассматривается стековая (stackfull coroutine).


Телепортация же никакого отношения к обещаниям и вовсе не имеет: это отдельный механизм.

Можно как-то обозначить проблему которую решает автор?

Решается задача взаимодействия объектов в многопоточной среде.


Если говорить о практических вещах, то для комуникации сейчас есть такие популярные модели как boost::asio, node.js, ZeroMQ, и много других, каждая из них предлагает решение каких-то проблем и асинхронность.

boost::asio — это про асинхронность на callback. Чтобы понять, о чем статья, надо сначала пописать достаточно немало на этих самых колбеках, а затем подумать о том, как сделать проще и лучше. И вообще, boost::asio, node.js, ZeroMQ — не модели взаимодействий, а способ решения конкретных задач. Поэтому получается сравнение апельсинов с телевизорами. Чтобы это понять, достаточно ответить на вопрос: "зачем node.js если есть акторная модель?" Думаю, вы поймете всю абсурдность претензий.


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

Для параллельных вычислений есть свои замечательные фреймворки. Однако они начинают плохо работать в условиях асинхронного взаимодействия: UI, сеть, диск и др.


Если мы говорим о низко-уровневых примитивах синхронизации (обертках над примитивами OS) — то и Mutex/SpinLock уже тоже немного устарели в угоду condition variables…

Этот оксюморон может и имел место быть, если бы condition variables не использовали мьютексы в своих интерфейсах.


Опять же в C++ новом стандарте есть опять же свои обертки (std::future). Зачем выдуманы эти все классы?

std::future — это недоразумение. Там даже нельзя запустить задачу в своем пуле потоков.

std::future — это недоразумение. Там даже нельзя запустить задачу в своем пуле потоков.
А зачем, собственно?

Это и резюмирует сомнительность мотивации за этой статьей: хотел создать велосипед и попиарится за счет Яндекса.
std::future — это недоразумение. Там даже нельзя запустить задачу в своем пуле потоков.

Простите, а кто вам мешает это сделать?


Реальная проблема std::future в другом: нет возможности зарегистрировать продолжение, из всех способов получения значения доступно только синхронное ожидание.

Простите, а кто вам мешает это сделать?

Ну я имел в виду std::async, который возвращает std::future.


Реальная проблема std::future в другом: нет возможности зарегистрировать продолжение, из всех способов получения значения доступно только синхронное ожидание.

Все так.

boost::asio — это про асинхронность на callback. Чтобы понять, о чем статья, надо сначала пописать достаточно немало на этих самых колбеках, а затем подумать о том, как сделать проще и лучше

Асинхронность на callback не отменяет возможности корутин. Они кстати есть в boost::asio, посмотрите.
По существу, отличие асинхронности на callback от асинхронности на OS-потоках только в том что в первом случае мы пишем свой собственный scheduler, а во втором пытаемся подстроится под scheduler в OS. А поскольку специализированный scheduler под конкретную задачу обычно и проще, и быстрее, чем универсальный, практически все современные асинхронные комуникационные фреймворки работают на модели каллбэков. А поверх этой штуки уже надстраивается даже корутины (Python 3 asyncio, boost::asio coroutines), если кому-то так важен этот syntax sugar, вот в Node.JS людей устраивают promisе (меня кстати тоже).

И вообще, boost::asio, node.js, ZeroMQ — не модели взаимодействий, а способ решения конкретных задач.

Именно, они сделаны для конкретных задач, но при этом изза своей гибкости эти фреймворки так легко интегрируются в существующие приложения что становятся моделями взаимодействия. Например я могу спокойно взять win32/QT/какое-то еще приложение и внедрить в него boost::asio комуникацию пользуюясь одним тредом для всего (UI, сеть, даже работа с файлами, кроме каких-то CPU-intensive вычислений) (встроив boost::asio::service (scheduler) в обычный UI цикл)… и строить эффективную асинхронную работу всего приложения. А что с вашими классами? Практически все UI фреймворки/API — однопоточны. Пытатся из UI работать с многопоточным API по своей удобности напоминает работу с радиоактивными отходами — обязательно полный цикл мер и защит от смертельной радиации (асинхронности OS schedulerа).

Для параллельных вычислений есть свои замечательные фреймворки. Однако они начинают плохо работать в условиях асинхронного взаимодействия: UI, сеть, диск и др.

Да. Только не забывайте что практически в любой OS работу с сетью, диск и так далее можно делать асинхронно без OS потоков (win32 completionports например), и это работает эффективней чем вызывать блокирующие API через потоки. Т.е. многопоточность на OS thread для сети и файлов попросту ненужна… и абсолютно все UI API что я знаю — в принципе однопоточны. Т.е. получается создание абстрактной модели под несуществующие задачи.

Этот оксюморон может и имел место быть, если бы condition variables не использовали мьютексы в своих интерфейсах.

Между прочим, считается что заимплементить корректные condition variables на базе mutex и event невероятно сложно (без возможности вмешатся в работу OS scheduler). Не хотите проверить свои силы?
И еще простая задача — попробуйте заимплементить multiple-read single-write синхронизацию на какой-то обьект (достаточно кстати реалистичная задача) на mutex и event, а потом на condition variable и сравните сложность имплементации а так же в какой случае вы допустили больше ошибок.

std::future — это недоразумение. Там даже нельзя запустить задачу в своем пуле потоков.

Я мало пользовался std::future но не вижу проблемы почему на базе этого api нельзя сделать свой thread pool? Почти всегда выгодней расширить существующее решение чем пытатся городить 100% новое просто потому что в существующем нехватает высоко-уровневой надстройки.

Да. Только не забывайте что практически в любой OS работу с сетью, диск и так далее можно делать асинхронно без OS потоков (win32 completionports например), и это работает эффективней чем вызывать блокирующие API через потоки. Т.е. многопоточность на OS thread для сети и файлов попросту ненужна… и абсолютно все UI API что я знаю — в принципе однопоточны. Т.е. получается создание абстрактной модели под несуществующие задачи.

Вот только вы забыли, что оконные сообщения в completionport не направить. А значит, в программе появляются как минимум два потока — поток UI и сетевой поток...


И еще сбоку пул потоков для вычислительных задач.

Можно наоборот, обработку completionports (или boost::asio::service) добавить в GetMessage UI цикл, почти в любом UI есть такой цикл. Главное чтобы обработчики UI сообщения не блокировались, а это не всегда возможно. Поэтому со 2ым потом может выйти даже проще.

Но если вернутся к статье… Она сейчас просто очень академическая… Поэтому и у практикующего читателя сразу возникает вопрос а чем оно лучше X/Y/Z? Ну и еще в корпоративной IT культуре помоему принято концепцию начинать с мотивационной части отвечающей на вопрос «зачем»… Поэтому возможно и получается что коментарии выглядят немного недружелюбными, надо снизить темп.

И как вы будете ее добавлять?


PS желание написать свой велосипед не нуждается в обоснованиях!

И как вы будете ее добавлять?

Самое простое — запустить completion ports/asio::service и обрабатывать сетевые евенты в приоритете (т.е. без задержки), а UI эвенты поллить по таймеру в сетевом фреймворке например 50 раз в секунду (цель получить стабильные 50 FPS). :)
Если есть свободное время то конечно можно и без поллинга через MsgWaitForMultipleObjectsEx/Overlapped/APC. Но вообще тупо проще создать 2ой поток для сети и не мудрить, оптимизация 1го треда не стоит потраченного времени на это.

Но вообще тупо проще создать 2ой поток для сети и не мудрить, оптимизация 1го треда не стоит потраченного времени на это.

А я о чем говорил?

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

Пока таких примеров нет, сложно судить о достоинствах предложенной модели. Да и прикидывать ее на задачи, с которыми сам имеешь дело, не так-то просто :(

Чисто с точки зрения здравого смысла возникает несколько вопросов:
— как программисту прикидывать, к каким накладным расходам приводит используемая им (или его коллегами) кухня? Например, написали некий класс Foo, обернули его в адаптер, потом накрутили вокруг него еще субъекторов. Как понять, насколько дорогим будет вызов Foo::bar?
— насколько просто будет работать с кодом, в котором Foo::bar может приостанавливать текущую короутину, запихивать функтор в канал, из которого кто-то что-то вычитает на другой рабочей нити, начнет обработку, телепортируется куда-то и т.д.? При том, что изменение одного слова в определении субъектора для Foo может кардинально поменять способ выполнения Foo::bar. Т.е. речь о том, что C++ и так ругают за то, что код на C++ отличается неочевидностью, здесь же неочевидность становится главной движущей силой.

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

PS. Отрадно, что разговоры в кулуарах конференции не прошли даром и появилось упоминание Симулы.
как программисту прикидывать, к каким накладным расходам приводит используемая им (или его коллегами) кухня? Например, написали некий класс Foo, обернули его в адаптер, потом накрутили вокруг него еще субъекторов. Как понять, насколько дорогим будет вызов Foo::bar?

Понять можно одним способом: профилированием кода. В этом контексте было бы неплохо сделать замеры на конкретных примерах, но это тоже потребует отдельной статьи.


насколько просто будет работать с кодом, в котором Foo::bar может приостанавливать текущую короутину, запихивать функтор в канал, из которого кто-то что-то вычитает на другой рабочей нити, начнет обработку, телепортируется куда-то и т.д.? При том, что изменение одного слова в определении субъектора для Foo может кардинально поменять способ выполнения Foo::bar. Т.е. речь о том, что C++ и так ругают за то, что код на C++ отличается неочевидностью, здесь же неочевидность становится главной движущей силой.

Тут надо смотреть на характеристику того или иного способа синхронизации. Если мы используем канал, то значит телепортация не позволит продолжит обработку и текущее исполнение будет ожидать телепортации обратно. В целом, исходя из опыта могу сказать, что лучше написать меньше кода, чем больше.


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

Субъектор появился в результате решения практической задачи про реплицированный объект. Когда есть большое число состояний, которое необходимо синхронизировать и следить за контекстом исполнения — то тут подобные абстракции сильно упрощают код.


В реальности, конечно, надо еще больше совершенствовать этот подход. И двигаться в сторону транзакционности, чтобы думать о синхронизации еще меньше.

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

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


Ну вот и надо бы рассказать про эту задачу, уверяю, далеко не все занимаются на C++ репликацией объектов по сети.

Ну вот здесь и рассказано: https://habrahabr.ru/post/267509/

Ну вот здесь и рассказано: habrahabr.ru/post/267509
Как по мне, там не рассказано. Ты там просто говоришь, что можно сделать репликацию объектов и она в коде будет выглядеть вот так. Но что за репликация, какие к ней требования? Как приложение, в котором репликация требуется, должно реагировать на ошибки?

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

Ты не поверишь, но мне тоже этого хочется. А пока есть то, что есть.


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


Писать более подробно некоторые вещи, которые я уже описывал в предыдущих статьях, я не вижу особого смысла. Попытаться раскрыть на примерах — про это я себе пометочку сделал. На самом деле, в статье примеры есть, и они отражают некоторые элементы и паттерны использования.

Ну я смотрю на это так: если после прочтения статей у многих читателей остается непонимание того, зачем это нужно, где, когда и как это использовать, то это недоработка автора статьи. И тут либо ты находишь возможность разъяснить людям свой подход, либо миришься с тем, что твой подход не понимают и не используют.

Понятное дело, что на более подробное описание требуется много сил и времени. Но что ж поделать-то, продвигать «в народ» что-либо всегда сложно.
И тут либо ты находишь возможность разъяснить людям свой подход, либо миришься с тем, что твой подход не понимают и не используют.

Всему свое время.

Именно!
Ничего подобного пока не использовал, поэтому интересно: а не приводят ли такие гонки за производительностью к коду, который очень трудно дебажить?

Если у тебя что-то сложнее hello world, то в любом случае будет трудно дебажить. А дебажить гонки — это на любителя, поэтому лучше их вообще не допускать. Этот подход выпиливает целый класс трудноуловимых проблем.

Я скорее об отладчиках, насколько адекватно они переваривают происходящее :)
Но возможно отсутствие инструментов — действительно меньшее зло, чем data race.

Сопрограммы показываются как обычные потоки с нормальным стек трейсом. Единственный момент: отладчик может показать только те сопрограммы, которые активны, т.е. сидят на потоках. Замороженные сопрограммы не видны, если не предпринимать специальных действий.

Отличная статья.

Было бы интересно увидеть как выглядит график стеков собранный через perf record для приложения использующего такую модель. Какое количество от общего времени работы будут занимать используемые примитивы синхронизации?

Так же интересно как отслеживать такие вещи как длины очередей. Когда мы хотим выполнять много задач на каком-то конкретном потоке или пулле потоков. Те делаем много телепортов в 1 поток?
Было бы интересно увидеть как выглядит график стеков собранный через perf record для приложения использующего такую модель.

Вполне нормально выглядит. Так же, как и обычные потоки.


Какое количество от общего времени работы будут занимать используемые примитивы синхронизации?

Зависит от реализации. Текущая реализация не самая эффективная по ряду причин. Но известно, как это поправить, чтобы стало сильно лучше. Тогда и бенчмарки можно будет провести.


Так же интересно как отслеживать такие вещи как длины очередей.

Через метрики, статистики и логи. Всё как всегда.


Когда мы хотим выполнять много задач на каком-то конкретном потоке или пулле потоков. Те делаем много телепортов в 1 поток?

Не понял вопроса.

Замечательная статья!
В защиту автора (хотя, мне кажется, он не нуждается в защите) выражу свое мнение: подход, основанный на истинных stackful сопрограммах, который на протяжении уже нескольких лет разрабатывает автор, мне кажется очень перспективным. И намного более глубоким и красивым, чем то, что предлагают ввести в стандарт C++. Стандарты всегда запаздывают, так как описывают вещи для всех уже привычные и тысячу раз испробованные.
Недостатками этого подхода мне видится две вещи:
1. Отдельный стек для каждой сопрограммы. На самом деле, это достоинство (все данные — на стеке, всегда под рукой, сколько бы нас не прерывали), но за которым надо внимательно следить, иначе при чрезмерном увлечении можно всю память сожрать. Быть может, стоит ограничить размер стека сопрограмм, но это уже детали.
2. Необычность. Этот «недостаток» лечится только опытом. Во-первых, надо попробовать все другие методы асинхронного/многопоточного программирования, начиная от тривиальных mutex/condvar, наплодить 100500 потоков на 4-ядерной машинке, удивиться «а чё оно так медленно работает и падает/лочится», затем пройти через callback hell, попробовать переписать callback на stackless сопрограммах, понять, что просто это не удастся, надо много переписывать. Во-вторых, надо научиться думать в терминах модели stackful сопрограмм; это бывает весьма нелегко, — заставить свой мозг работать по-другому, в терминах новой модели. Например, синхронизировать вовсе без примитивов синхронизации. Или: сделать многопоточную программу, в которой многопоточная логика не вылезает наверх, не затеняет логику самой программы, так что, читая код, вы видите, что делает программа, а не потроха взаимодействия потоков, future, callback и прочую машинерию, за которой леса не видно.

Вообще, трудность введения новых моделей взаимодействия компонентов программы всегда одна и та же: программу приходится переписывать практически с нуля. И вторая трудность — выбор правильной модели.

PS: сам я модель stackful сопрограмм не применял по причинам, описанным выше, — негде было. Но искренне завидую тем, кто это попробовал на реальных задачах.

Спасибо! Этот комментарий должен был быть первым.

В стандарт C++ предлагается включить stackless сопрограммы не потому что они "уже привычные и испробованные", а потому что они требуют обязательной поддержки со стороны компилятора. Вариант же stackfull может быть добавлен в язык сторонней библиотекой, что и демонстрирует boost::context

Это крайне слабый аргумент. По этой причине тогда не нужно было включать boost::filesystem, т.к. не нужно изменять компилятор. Тем не менее, это было сделано.


Стандарт — он не только для компилятора, но и для расширения существующей библиотеки и интеграции разных кусков воедино.

Я считаю, что надо и stackless, и stackful. Дело в том, что для простых однопоточных генераторов нет ничего лучше stackless сопрограмм, т.к. их компилятор при желании может превратить в обычные циклы без аллокаций памяти. И это действительно круто и стоит того.


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


Зачем нужен стандарт для stackful? Для того, чтобы различные инструменты научились их поддерживать без танцев с бубнами. Чтобы дебагеры их обнаруживали, чтобы санитайзеры из видели и т.д. Т.е. чтобы все работало из коробки так, как будто это нативные инструменты. Когда есть единый инструмент, тогда он будет вылизан и отточен. Поэтому его тоже надо в стандарт.

Но начинать-то надо все равно со stackless. Хотя бы для того чтобы стандартное API по два раза не переделывать. Потому что stackless в stackful можно без труда превратить, а вот обратное превращение съест все выгоды stackless-подхода.

Потому что stackless в stackful можно без труда превратить
Это каким образом?

stackless сопрограмма возвращает std::future<...>. Достаточно вставить внутрь методов .get и .wait поставить проверку не вызваны ли они из stackful сопрограммы, с адекватной реакцией на этот факт — и все, любые stackless сопрограммы можно вызывать изнутри stackful сопрограммы. А вот в обратную сторону так не работает.


Механизмы же для асинхронного ввода-вывода требуются одни и те же, и там stackless-версия получается даже проще чем stackful-версия.

Это звучит несколько странно, конечно. С тем же успехом можно сказать, что можно написать программу на колбеках, а затем переписать ее на корутины. Только зачем?


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

На него легко переписать верхние слои, но не нижние.


На нижнем уровне нужно писать альтернативу для каждой блокирующей функции — и здесь-то совершенно не важно что планируется использовать, stackful сопрограммы, stackless сопрограммы или колбеки: все равно все закончится проактором.


Поэтому имеет смысл переписать на stackless сопрограммы чтобы не делать одну и ту же работу два раза.

А я вообще не вижу смысла использовать разные подходы, когда можно использовать один подход, и уже сегодня.


В чем профит?

Я считаю, что надо и stackless, и stackful

опять же, stackful доступны в виде библиотек. Чтобы получить и те и другие, необходимо добавить stackless в стандарт

Чтобы получить и те и другие из коробки, надо добавить оба в стандарт.

Чтобы получить и те и другие, необходимо и достаточно* добавить stackless в стандарт
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории