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

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

Огромное спасибо за статью! Дорога с работы будет познавательной :)

Оффтоп: Скриншоты из фильма «Десятое королевство»?
Ага. Теги как бы намекают…
Эх, помню времена когда ни boost, ни лямбд не было.
Только кучка теплого, лампового, платформо-зависимого кода…
Да, который при портировании вызывает адскую попоболь…
Спасибо спасибо спасибо.
Отличная статья.
Да, я старался. Времени ушло на подготовку статьи — уйма. Учитывая то, что я ее начинал делать в начале этого года. Потом конференции, потом еще… Надеюсь, время тратил не зря.
В чем выгода параллельного выбора из Mem Cache и Disk Cache? Дисковый кеш заведомо в тысячи раз медленней. При каких конкретно условиях выборка из Disk Cache осуществится быстрее чем из Mem Cache?
Надо понимать, что в статье приведен модельный пример, который раскрывает использование многих примитивов. В частности, иногда хочется получить результат параллельно из нескольких источников. Это может быть как получение данных из памяти и диска, а может быть из двух дисков: один — SSD, другой — обычный. SSD быстрый, но много не влезет, а HDD медленный, но влезает сильно больше. Но оба они не сказать, чтобы быстрые, поэтому в такой постановке это будет иметь больше смысла. В другом случае могут быть удаленная база данных и локальный кеш на диске.

В общем, сути это не меняет. Хочется параллелить операции по получению данных, и хочется делать это наиболее простым способом. Как? Показано в статье.
Обратите внимание на невиртуальные деструкторы и корректное разрушение объектов! Хотя кое-кто утверждает, что при наследовании всегда необходимо использовать виртуальные деструкторы.

Вы только что разрушили мой мир. Я конечно не гуру С++, но видовал уже многое подкапотное, но что бы такое… я честно не понимаю — КАК? Как Вы в иерархии классов БЕЗ виртуального деструктора корретно удаляете объек наследник?
Объект-наследник корректно удаляется просто потому что его удаляют через указатель его собственного типа, не приводя к базовому. Посмотрите реализацию gcnew.

Примерно такой же механизм используется в реализации shared_ptr.
Если быть точнее, то в реализации связки shared_ptr + make_shared.
Ну, shared_ptr можно и без make_shared создать — механизм все равно будет задействован.
Да, точно, т.к. принимается шаблонный указатель. Т.е. такие строчки будут вызывать корректное освобождение памяти даже без виртуального деструктора в A:

std::shared_ptr<A> a = std::make_shared<B>();
std::shared_ptr<A> a(new B());
Спасибо. И вправду никакой магии, почти…
Вам пора уже написать нормальную (красивую, с кросс ссылками) документацию и опубликовать ее в сети. Статьи на хабре это конечно хорошо, но как дополнение и способ продвижения в массы.
Прикол в том, что документацию обычно никто не читает. Ну или не читают так, как читают статьи на хабре. Но я подумаю над этим.
Мне хотелось самому понять, как они работают. И потом иметь больший контроль над взаимодействием с ними. Когда я игрался с памятью в сопрограммах, то такая реализация мне сильно помогла.
Поздновато конечно, но я все же укажу на некоторые недоработки в реализации (если я конечно правильно разобрался со всем):

Лямбда которую мы засовываем в defer фактически выполняется уже после переключения контекста, вне корутины. И получается, что если там кидается исключение — корутина об этом не знает и не может обработать, исключение перехватывается менеджером задач и о нем никто не узнает, кроме логера. Я бы предложил решать проблему другим способом: отказаться от defer и при вызове resume — проверять, если корутина еще выполняется — выставлять ей специальный флаг — который проверится в ближайшем yield и если нужно поставит менеджеру потоков еще одну задачу на resume. По моему самое удачное решение, предложенные вами альтернативы в виде: ожидания на мьютексах yield — блокируют надолго поток, бустовый io_service::strand не работает с несколькими очередями задач.

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

На мой взгляд удачнее будет в класс с корутиной добавить массив времени истечения таймаутов, а таймер будет в конструкторе добавлять элементы в массив, а в деструкторе удалять, все что не удалилось — будет проверятся в yield и других «контрольных точках» на предмет истечения лимита времени.

Да и спасибо за популяризацию корутин.
Лямбда которую мы засовываем в defer фактически выполняется уже после переключения контекста, вне корутины. И получается, что если там кидается исключение — корутина об этом не знает и не может обработать, исключение перехватывается менеджером задач и о нем никто не узнает, кроме логера. Я бы предложил решать проблему другим способом: отказаться от defer и при вызове resume — проверять, если корутина еще выполняется — выставлять ей специальный флаг — который проверится в ближайшем yield и если нужно поставит менеджеру потоков еще одну задачу на resume. По моему самое удачное решение, предложенные вами альтернативы в виде: ожидания на мьютексах yield — блокируют надолго поток, бустовый io_service::strand не работает с несколькими очередями задач.
Да, есть такое. Поэтому фактически, код внутри defer — асинхронный, а значит — не может и не должен кидать исключение. Смысл тут в том, чтобы все такие вызовы обернуть и больше никогда не использовать defer.

Я этот момент планирую переписать, defer мне тоже перестал нравиться. Хотя изначально идея была великолепная.

Таймауты: т.к. они выполняются в качестве отдельной задачи, возможно в другом потоке и без синхронизации, возможны некоторые нехорошие ситуации:
н-р процессор доходит до «goer.timedout();» приостанавливает поток и начинает выполнять другие потоки, в том числе и нашу корутину, где должен произойти таймаут, успешно выполняет еще кучу кода и переключается на «goer.timedout();» и исполняет. В итоге получим таймаут в совершенно другом месте, где мы вообще никаких таймеров не ставили.
Ну тут я не вижу проблем: если таймаут случился, то он может сработать и позже, т.к. все равно полетит исключение. Другое дело — а что, если мы эти исключения обрабатываем? Тогда да, неприятность может возникнуть. Но она скорее умозрительная, т.к. скорее всего после таймаута все что нужно сделать — это подчистить за собой и отменить всякие действия, что должно выполняться автоматом с использованием RAII.

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

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

Я этот момент планирую переписать, defer мне тоже перестал нравиться. Хотя изначально идея была великолепная.


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

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


Даже если не обрабатывать могут случатся плохие ситуации. Если исключение кидается в том месте где мы ожидали, мы можем с использование RAII выстроить корректный откат, но ведь мы можем выйти из этого блока кода и перейти в следующий:
{
  Timeout t(500);
  socket->Send(...);
  yield();
  answer = socket->ReadAnswer(...);
  // в этом блоке кода мы ожидали исключений, написали код, который может откатить изменения
}
{
  cache1->Add(answer);
  ...
  yield();
  // исключение реально произошло вот тут, рассогласовав нам кеши, мы его тут совсем не ожидали, кеши у нас были железобетонные не кидающие исключений
  ...
  cache2->Add(answer);
}

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

Даже если не обрабатывать могут случатся плохие ситуации. Если исключение кидается в том месте где мы ожидали, мы можем с использование RAII выстроить корректный откат, но ведь мы можем выйти из этого блока кода и перейти в следующий:
Для этого как раз есть EventsGuard, который позволяет «опасный» кусок изолировать. Необходимо также помнить, что в любой момент сопрограмму можно отменить, и код к этому должен быть готов. Поэтому только RAII, только нормальный C++. Однако если хочется грязных хаков, то, как я уже говорил, есть EventsGuard.

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


Кстати, вызывать handleEvents из деструктора — нельзя ни в коем случае! Это приведет к std::terminate… Вы, конечно, и сами std::uncaught_exception() проверяете — но фокус в том, что единственное предназначение handleEvents — это кинуть исключение, то есть сделать то чего делать категорически нельзя.


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

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

Речь шла про EventsGuard. Грязный хак — это фигура речи.


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

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

Проблема срабатывания тайм-аута после выхода из блока где этот тайм-аут был объявлен.

А как решится проблема через лямбды? Сделать очередь лямбд на выполнение?

Да. Или использовать ctx::exec_ontop_arg — но это только над приостановленной сопрограммой так можно.

Или использовать ctx::exec_ontop_arg

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

Зарегистрируйтесь на Хабре , чтобы оставить комментарий