Comments 61
Ну и вопрос не в том, как именно будет выполняться работа в отдельном потоке. А в том, как потоки будут обмениваться информацией. Ведь поток существует не сам по себе, а чтобы взять что-то на себя, сделать это и отдать кому-то результат. И вопрос в том, как взаимодействие организовать, чтобы не получить гонок, дедлоков и, при этом, не тормозить.
Я немного радикальнее считаю.Так это хорошо. Нет, серьезно. Когда вы уверены, что у вас есть хороший способ решать определенный класс задач, то это здорово.
Но целью статьи было не показать, что какой-то подход лучше для обслуживания сложных HTTP-запросов, а какой-то хуже. Целью было показать наличие разных подходов, которые могут применяться разработчиками помимо работы с threads, mutexes, condition_variables, barriers и т.д. А задача такая для иллюстрации была выбрана потому, что она простая, понятная многим и, что важно, все три показанных подхода вполне применимы для ее решения. Тогда как data flows и reactive programming, рассказ о которых в доклад бы не поместился, для решения такой задачи были бы вряд ли уместны.
Дедлоки отлаживать проще простого — аттач дебаггером к стоящему серверу, распечатка стеков всех тредов, и через полчаса все понятно.Видимо, мне везло сильно меньше, чем вам.
а решение выносить на уровень вышеА вы могли бы раскрыть мысль? Вот один актор делает send другому актору. И внутри send-а выясняется, что почтовый ящик получателя полон. Решение нужно принимать внутри send-а. О каком «уровне выше» вы говорите?
а можем выкинуть ошибку и прекратить обработку входящих запросов на уровне сервисаМне кажется, что вы немного путаете уровни. У вас некий сервис реализован как совокупность общающихся акторов. Чтобы прекратить обработку сервисом входящих запросов вам нужно, чтобы какие-то акторы из его реализации распознали и среагировали на переполнение почтового ящика одного (или не одного) актора. И вопрос реализации вашего сервиса будет в том, как вы это будете делать (точнее, это будет один из вопросов).
Что делать send'у зависит от реализации, а реализация зависит от поставленных требований. send может вернуть ошибку и тогда актор А будет думать, что делать дальше.
какие-то акторы из его реализации распознали и среагировали на переполнение почтового ящика одного
Разве? Если send вернул актору A ошибку, то актор А может отправить сообщение супервизору(если использовать терминологию из Erlang), а тот уже может приостановить подключение новых соединений.
А можно сделать так, как вы предложили. Супервизор мониторит размер очередей у акторов, т.к. актор можем быть проактором.
Разве?Да. И вы сами именно об этом и пишете:
Если send вернул актору A ошибку, то актор А может отправить сообщение супервизоруЭто и есть распознавание проблемы и реакция на нее.
На практике, правда, есть важный нюанс: результат send-а, как правило, никто не контролирует.
Во всех actor-фреймворках результат send'а куда-нибудь, да валится.Простите, не понял этой фразы. Речь о том, что результат (успех/ошибка) доступен тому, кто вызвал send?
Если так, то да. Наверное, нет фреймворков, в которых нельзя узнать результат send-а.
А значит, вопрос контроля — это вопрос волеизъявления/знаний разработчика.А это уже масло-масляное. Очевидно, что разработчик должен контролировать то, что он делает.
Если бы send'ы неконтролируемо могли бы потеряться, говорить о детерминированности actor-процессов не приходилось бы.Здесь опять не понял.
Я говорю о том, что на практике мало кто пишет код так, чтобы проверять результат каждого send-а и предпринимать соответствующую реакцию на каждый неудачный send.
И их полку прибывает, в последнее время.
А дедлоки отлаживать действительно не сложно (когда есть физический доступ к оборудованию с дедлоком). Сложнее их фиксить, а еще сложнее добиться чтобы их не было.
Поэтому и расцвели теоретики по actors, immutables, pure functions — чтобы дедлоков в принципе не было.
все нижеследующие примеры не привязаны к какому-то конкретному фреймворку или библиотеке. Любые совпадения в именах API-ных вызовов являются случайными и непреднамеренными.
Шикарная формулировка, откровенно порадовали))))
Спасибо за статьюСпасибо за отзыв!
Последняя модель, таск, читается с трудом и не позволяет конфигурировать связи в runtime из внешнего конфигаИмхо, Task-based подход имеет смысл там, где задачи формируются «по месту», в зависимости от того, что пришло на вход. И когда при обработке разных входящих воздействий можно строить разные наборы тасков.
То, о чем говорите вы — это уже из области task graph-ов, которые реализованы в Intel TBB. Но рассказ об этом подходе в доклад бы уже не поместился.
Спасибо за статью! Ждём теперь статью по Data flow!
По акторам — книга автора QP (Miro Samek, «Practical UML Statecharts in C/C++»).Не читал.
«Взаимодействующие последовательные процессы»ИМХО (здесь не зря ИМХО большими буквами, все очень и очень субъективно) эта книга хороша для студентов профильных ВУЗов, для исследователей в области computer science, для специалистов по верификации программ. Как эта книга может помочь при проектировании конкретного прикладного решения на базе CSP, для меня лично не понятно. И да, CSP-book — это и есть тот самый «матан», без которого хотелось в докладе обойтись в разговоре про CSP.
И еще одна очень интересная особенность Task-based похода — это отмена задач если что-то пошло не так. В самом деле, допустим, мы создали 150 задач, выполнили первые 10 из них и поняли, что все, дальше работу продолжать нет смысла. Как нам отменить 140 оставшихся? Это очень и очень хороший вопрос :)
Еще один похожий вопрос — это как подружить задачи с таймерами и таймаутами.
Насколько я понимаю, Task-подход, это то, как работают современные rx-фреймворки.
А в современных rx-фреймворках все хорошо, с таймерами, таймаутами, отменами, контролем ошибок, и пр.
Следовательно, Actors/CSP и Tasks не столько противостоят друг другу, сколько дополняют друг друга. Actors/CSP могут использоваться для декомпозиции задачи и определения интерфейсов между компонентами. А Tasks затем могут использоваться в реализации конкретных компонентов.
Они действительно взаимодополняют друг друга.
Стоит только отметить, что Actors/CSP могут физически быть на разном железе. Тогда как описанный Task-подход, это то как устроена утилизация многоядерности на одной железке. (шарить треды в общем то некоторые ОС умеют, но массового применения это не нашло)
А что есть задача? Чем она имплементируется? Наверно некоторая субстанция, которая что-то делает в отдельном потоке. Ой, опять вернулись к потоку.
При этом я понимая, что async/await это языковые конструкции. А rx-фреймворки — набор библиотек, возможно даже без async/await (ну нет их в конкретной языковой платформе, что поделать) Но чем они принципиально отличаются с точки зрения общей механики. И там, и там отдельные потоки, и там, и там, присутствует в том или ином виде event loop, даже если он не называется event loop.
И чем отличается асинхронная задача, от потока событий, который порождает ровно одно событие?
потока событий, который порождает ровно одно событие?Тем, что это оксюморон. Ну вроде «живой мертвец».
Лишено практического смысла. Нет, конечно в камментах на Хабре об этом можно потрепаться, но зачем это использовать при разработке софта?
Вот есть у вас обращение к удаленному сервису. Обернули вы вызов в Observable, и так как удаленный сервис возвращает один результат, ваш Observable тоже возвращает один результат — одно событие. И такой подход не является biased, а вполне практичный, никаких живых мертвецов.
Обернули вы вызов в ObservableОбернули что именно? Кто, где и как будет вызов осуществлять?
Кто, где и как будет вызов осуществлять? -> вызов дергает какойнибудь subscriber
Когда мне нужно сделать какую-то операцию (например, загрузить изображение), то я создаю некий observable объект. Скажем, ImageDownloadTask.
На появление этого объекта реагирует какой-то подписчик, который знает, как обслуживать такие объекты. Скажем, это будет ImageDownloadTaskPerformer.
Этот ImageDownloadTaskPerformer получает ImageDownloadTask как входящее событие и выполняет загрузку изображения.
После чего, как я понимаю, должен возникнуть еще один объект, скажем, DownloadImageInstance, на возникновение которого должен еще кто-то среагировать.
Правильно? Если нет, то раскройте свою мысли пошире, пожалуйста.
Задача — это абстракция некоторого значения, которое будет доступно в будущем. Видите, я обошелся без использования слова "поток".
И чем отличается асинхронная задача, от потока событий, который порождает ровно одно событие?
Набором гарантий. Если вам дан на вход произвольный поток событий — вы не можете сказать сколько их там будет. Соответственно, нужно или обрабатывать общий случай — или писать хрупкий код. Если же вы на вход получили задачу — вы всегда можете считать что событие будет ровно одно.
Вот вам реализация задачи на javascript: github.com/taylorhakes/promise-polyfill/blob/master/src/index.js
Попробуйте найти тут хоть что-нибудь про потоки…
Насколько я понимаю, Task-подход, это то, как работают современные rx-фреймворки.Я понимаю совсем по другому. RX-подход — это когда у нас есть поток событий и этот поток валится в одну точку входа (будь то задача, нить, сопрограмма или еще что-то). И уже в этой точке происходит обработка очередного события. Возможно, с использованием знаний о предыдущих событиях (т.е. с использованием состояния обработчика).
Поскольку есть поток событий, то в этот поток элементарно добавляются и события от таймера.
В случае с task-based подходом у нас нет потока событий. Есть конкретный таск, который должен выполниться на конкретном контексте при возникновении конкретных условий (в простейшем случае когда будет готов future, на который таск «повесили»). И, что важно, таски создаются «по месту», для обработки конкретной операции. Количество, типы и взаимосвязи между тасками определяются в зависимости от этой самой конкретной операции.
Стоит только отметить, что Actors/CSP могут физически быть на разном железе. Тогда как описанный Task-подход, это то как устроена утилизация многоядерности на одной железке.Речь шла только об использовании Actors/CSP для упрощения многопоточности. Возможность создавать распределенные приложения на базе Actors/CSP выходит за рамки этого разговора.
Отличная статья, я, вроде бы, наконец понял, что такое каналы в Го :)
В целом, callback hell решается средствами языка (async/await в C#, TypeScript, Javascript).
Отличная статьяСпасибо!
callback hell решается средствами языка (async/await в C#, TypeScript, Javascript).У меня, к сожалению, нет опыта работы с async/await и этими ЯП, не могу прокомментировать.
Примерно так это выглядит (код нельзя использовать в продакшене).
var task1 = new Task(() => DoStuff());
var task2 = new Task(x => DoStuff2(x));
task1.ContinueWith(x => DoStuff2(x.Result)).Wait();
А вот на асинк-авейт
var x = await DoStuff();
await DoStuff(x);
Компилятор за тебя строит стейт-машину и прочее. Это имеет свои минусы, но в целом ок.
Спасает. В C# не надо юзать ContinueWith и Wait совместно с await, велика вероятность словить дедлок на SyncronizationContext. Поэтому, если ты используешь await, у тебя везде await и код выглядит как синхронный.
async([]{
...
return async(do_something([=]{
...
return async(do_something_else([=](auto x) {
if(x) return async(do_another_thing([=]{ ... }));
else return async(do_different_thing([=]{ ... }));
}));
}));
});
В случае async/await, как я понимаю, все это нужно будет записать линейно в обратном порядке. При этом опять же несколько теряется контекст, т.к. сначала нам нужно описать вложенные блоки, которые будут зависеть от значений и/или параметров, вычисляемых в последующих блоках.
Т.е. можно получить ту же лапшу, но намотанную в другом направлении.
Ну можно, конечно. Но лапшу можно получить на ровном месте просто так. Можно и на коллбэках ведь писать аккуратный код =)
Но проблема в том, что в С++ здесь еще непаханное поле. И как оно все будет нужно еще посмотреть, попробовать, набить шишек.
Откуда возьмется обратный порядок? приведенный вами код будет выглядеть как-то так:
async([] -> void {
...
co_await do_something();
...
auto x = co_await do_something_else();
if (x) {
co_await do_another_thing();
...
} else {
co_await do_different_thing();
...
}
});
auto x = co_await wrap_async(do_something_else);Вы решили, что x — это результат вычисления do_something_else. У меня в коде в do_something_else передается лямбда, которой кто-то отдаст x в качестве параметра.
Соответственно, результат wrap_async-а для do_something_else не может использоваться для выбора между do_another_thing и do_different_thing.
А зачем do_something_else будет возвращать ID запроса, если мы пишем код на задачах, а не на колбеках? Пусть возвращает задачу!
Ну а если это какая-то внешняя функция, то написать для нее обертку будет нетрудно:
decltype(auto) do_something_else_wrapped() {
struct awaiter {
bool await_ready() { return false; }
void await_suspend(coroutine_handle<void> handle) {
do_something_else([=] (bool x) {
this.result = x;
handle.resume();
});
}
bool await_result() { return result; }
bool result;
};
return awaiter{};
}
А зачем do_something_else будет возвращать ID запросаЧтобы этот ID можно было куда-то сохранить, например, в список активных в данный момент запросов?
Кстати, а что у вас за волшебная функция async, перегрузки которой принимают то лямбду, то ID запроса?
Кстати, а что у вас за волшебная функция async, которая принимает ID запроса?Это следствие того, что писался абстрактный пример в вакууме, да еще после переключения с совершенно другой темы. Там следовало бы написать что-то вроде:
async(do_something_else, [=](auto x){...});
чтобы лямбда шла аргументом в do_something_else.Значит, нужно либо переписать do_something_else, либо обернуть в do_something_else_wrapped как я написал выше, после чего спокойно вызывать через co_await.
Текстовая версия доклада «Actors vs CSP vs Tasks...» с C++ CoreHard Autumn 2018