Resumable функции

http://meetingcpp.com/index.php/br/items/resumable-functions-async-and-await.html
  • Перевод
  • Tutorial
На прошлой неделе в мире С++ произошло интересное событие. Компания Microsoft объявила о выходе обновления к компилятору С++ в Visual Studio 2013. Само по себе обновление компилятора отдельно от Visual Studio или её сервис-пака — уже нетривиальное для Microsoft событие. Но ещё интереснее то, что вошло в это обновление. Полный список можно почитать по ссылке выше, а я остановлюсь только на одном моменте — resumable функции. Для полного понимания ситуации: Microsoft изрядно протроллила и комитет по стандартизации С++ и разработчиков gcc\clang, выпустив (тут надо внимательно) реализацию экспериментальной и не утверждённой ещё возможности будущего стандарта C++17, основанной на экспериментальных и не утверждённых ещё возможностях будущего стандарта C++14, которые в свою очередь являются исправлениями не сильно ещё вошедших в повседневное программирование возможностей С++11.

Достаточно гиковский ход, не находите?

А ниже будет перевод статьи с meetingcpp.com, рассказывающей о том, что это за фича и как её использовать.


На прошедшей недавно конференции BUILD Херб Саттер рассказывал о будущем языка С++. Его доклад был полон красивых примеров на С++11 и С++14. И вдруг, прямо из ниоткуда — resumable функции. Херб — один из авторов документа, описывающего std::future и resumable функции, так что само их упоминание не было для меня сюрпризом, но вот что меня удивило, так это то, сколько внимания он уделил этой теме и упоминание того факт, что resumable функции войдут в обновление к Visual Studio 2013 (пусть не в сам релиз VS2013, но всё же намного раньше следующей версии IDE).

Я начну с небольшого спойлера: это возможность как минимум С++1y, она не войдёт в С++14, но в дальнейшем именно асинхронное и параллельное программирование будет в тренде развития языка, так что resumable функции станут органичной частью нового стандарта. В дальнейшем эта фича будет поддерживаться всеми компиляторами, а на данный момент Microsoft шагает впереди планеты всей с собственной реализацией. Не секрет, что данная функциональность имеет некоторую аналогию с async/await из языка C#.

Что такое resumable функции?

Это, вообще-то и есть главный вопрос, который мы тут пытаемся выяснить. Прежде чем я начну объяснять, чем бы это могло быть и как их определяет документ N3650, я должен сделать небольшую остановку и рассказать, что такое futures, поскольку resumable функции основаны на том предположении, что std::future будет расширено методом .then(), как это предполагается в документе N3634. future — это результат выполнения асинхронной операции. Это одно из базовых понятий асинхронного программирования. future — это место, где хранится информация о статусе выполнения асинхронной задачи и её результат, если он уже доступен. Вы можете вызывать метод get(), который дождётся завершения асинхронной операции и вернёт вам её результат (это уже реализовано в стандарте), либо зарегистрировать обработчик её завершения через метод .then() (который пока что не в стандарте). Отсутствие .then() в С++11 — одна из наиболее критикуемых ошибок, она наверняка будет исправлена в С++14, вместе с некоторыми другими улучшениями std::future.

С++11 добавил в С++ лямбды, так что в комбинации это даёт возможность построить цепочку асинхронных вызовов лямбда-функций (колбеков). Теперь будет возможно запустить выполнение асинхронной задачи и отреагировать на её завершение в обработчике, зарегистрированном через метод .then(). «Прочитать ответ сервера, then — распарсить его, then — обработать его, ...». С проверкой ошибок и логированием по ходу дела. Такой подход является обыденным делом в некоторых языках, но пока не в С++. Правильное понимание столь мощного механизма может серьёзно повлиять на то, как вы будете писать код в будущем.

Короткий пример, чтобы продемонстрировать std::future:

std::future<int> f_int = make_dummy_future(42);
int i = f_int.get() // ждём окончания работы функции
f_int.then([](std::future<int> i){/* deal with it */}) // регистрируем обработчик


Идея resumable функции в том, чтобы позволить компилятору самому позаботиться о построении цепочки futures, присоединённых друг к другу, и правильном их вызове через .then().
Достичь этого предлагается через объявление двух новых ключевых слов: async и await. Обратите внимание, в этом нет ничего общего с библиотекой std::async, это НЕ библиотека, это расширение языка программирования. Функция помечается ключевым словом async, после её объявления, но до её спецификации по генерируемым исключениям:

void resumable_function(int i) async


Так что теперь компилятор знает, что это resumable функция. И начинается веселье. Хотя это и функция, но всё же достаточно ограниченная по возможностям. Первое из них это её возвращаемый тип — он может быть либо void, либо std::future/std::shared_future. Возможно, типы, которые могут быть преобразованы к std::(shared_)future тоже будут разрешены, но вообще-то неявные преобразования — не лучшее решение здесь, так что, возможно, комитет решить выбрать строгое соответствие типов. Текущий документ ещё пока разрешает возвращать T, неявно конвертируя его в
std::future.

Внутри resumable функции вещи происходят тоже слегка иначе. Используя ключевое слово await теперь можно "завернуть" выражение или вызов функции в future, которая посчитает это выражение или вызов в другом потоке. Ключевое слово await здесь определяется как унарный оператор (аналогично, например, оператору "!").

И вот мы подоходим к интересному. Вы можете использовать ключевое слово await неоднократно - каждое его применение создаст std::future, которая начнёт выполняться в параллельном потоке. Давайте посмотрим на пример, который в своём докладе использовал Hartmut Kaiser - это рассчёт чисел Фибоначчи:

std::future<uint64_t> fibonacci(uint64_t n) async { if (n < 2) return std::make_ready_future(n); std::future<uint64_t> lhs = std::async(&fibonacci, n-1); std::future<uint64_t> rhs = fibonacci(n-2); return await lhs + await rhs; }


Вот так resumable функции будут выглядеть в коде. Оборачивание функции lhs в std::future не требуется, вы можете вызывать любую функцию с ключевым словом await, компилятор обернёт её в std::future за вас.

Как я писал ранее, resumable функции - специальный вид функций, поскольку первый await возвращает future вызвавшему и здесь всё становится чуть сложнее. Реализация механизма в компиляторе должна предусматривать нечто более сложное, чем просто стек функции, который будет уничтожен при первом же await. Реализация должна убедиться, что локальные данные функции будут всё еще достижимы коду за await. Но для программиста, который будет использовать этот механизм эти детали не должны играть роли, вся магия происходит за сценой.

Библиотечное решение

Когда я впервые познакомился с resumable функциями, одной из моих мыслей было "разве всё это нельзя реализовать без изменений языка?". Ответ - можно. Думаю, многие из читателей могли бы представить себе библиотечное решение с похожей функциональностью. В resumable функциях почти нет выигрыша по производительности работы скомпилированного кода. Вот каким образом Thomas Heller продемонстрировал предыдущий пример (рассчет чисел Фибоначчи) без resumable функций.

std::future< uint64_t> fibonacci(uint64_t n)
{
    if (n < 2) return std::make_ready_future(n);

    std::future<uint64_t> lhs_future = std::async(&fibonacci, n-1); //.unwrap();
    std::future<uint64_t> rhs_future = fibonacci(n-2);

    return
        dataflow(
            unwrapped([](uint64_t lhs, uint64_t rhs)
            {
                return lhs + rhs;
            })
          , lhs_future, rhs_future
        );
}


Вот так оно может выглядеть. Но заметьте, "dataflow" будет иметь семантику await только являясь последним выражением функции. Только в этом случае можно вернуть объект future, соответствующий общему результату. Так что с С++11 или С++14 мы уже можем кое-что сделать и на уровне библиотек.

Так зачем изменять язык и вводить какие-то там resumable функции?

Как я писал выше, нет никаких прямых и видимых причин с точки зрения производительности, однако данное решение чуть более элегантно и имеет некоторые другие преимущества. Я разговаривал с Hartmut Kaiser об этой фиче и он дал мне чётко понять, что считает resumable функции хорошим решением. Он заметил, что благодаря возможности сохранять локальное состояние функции в некоторых случаях мы получим экономию на выделении\освобождении памяти. Библиотечные решения так или иначе будут вынуждены выделять и освобождать стек для вызовов своих функций, а вот решение на уровне языка может применить более эффективные приёмы.

Другие преимущества resumable функций

Скорость и производительность - не единственное (а может быть даже и не главное) в resumable функциях. Более приятное - сам их синтаксис, его легкость, простота и изящность. Вы можете просто начать писать код с использованием async/await, наряду с базовыми конструкциями языка типа (if/else, for и т.д.). Код становится чище. Вот пример из документа N3650, в начале с использованием только std::future:

future<int> f(shared_ptr str)
{
  shared_ptr<vector> buf = ...;
  return str->read(512, buf)
  .then([](future<int> op)// lambda 1
  {
    return op.get() + 11;
  });
}

future<void> g()
{
  shared_ptr s = ...;
  return f(s).then([s](future<int> op) // lambda 2
  {
  s->close();
  });
} 


А теперь напишем то же самое на resumable функциях:

future<void> f(stream str) async
{
  shared_ptr<vector> buf = ...;
  int count = await str.read(512, buf);
  return count + 11;
}

future g() async
{
  stream s = ...;
  int pls11 = await f(s);
  s.close();
}


Код с resumable функциями стал короче и намного лучше читаемым (что, на самом деле, самое важное в коде). Но настоящие преимущества начинают проявляться когда асинхронный код комбинируется с базовыми управляющими элементами языка. Я покажу вам короткий пример, который привел Herb Sutter в своём докладе на BUILD:

std::string read( std::string file, std::string suffix ) {
   std::istream fi = open(file).get();
   std::string ret, chunk;
   while( (chunk = fi.read().get()).size() )
      ret += chunk + suffix;
   return ret;
}


Это простой пример "синхронной асинхронности" - в коде используется future::get() чтобы дождаться результата асинхронной операции в std::future. Неплохо было бы улучшить и ускорить этот код, заменив get() на then(). Давайте посмотрим, что выйдет.

task<std::string> read( std::string file, std::string suffix ) {
   return open(file)
   .then([=](std::istream fi) {
      auto ret = std::make_shared<std::string>();
      auto next = 
         std::make_shared<std::function<task()>>(
      [=]{
         fi.read()
         .then([=](std::string chunk) {
            if( chunk.size() ) {
               *ret += chunk + suffix;
               return (*next)();
            }
            return *ret;
         });
      });
      return (*next)();
   });
}


Для того чтобы использовать .then() корректно нам пришлось слегка усложнить код. А теперь давайте посмотрим как то же самое могло бы выглядеть на async/await:

task<std::string> read( std::string file, std::string suffix ) __async {
   std::istream fi = __await open(file);
   std::string ret, chunk;
   while( (chunk = __await fi.read()).size() )
      ret += chunk + suffix;
   return ret;
}


В обоих случаях возвращаемое значение должно быть типа task<std::string>, поскольку на момент возврата оно всё ещё может быть в процессе рассчёта. Версия с использованием await значительно проще, чем версия с .then(). Данная реализация использует ключевые слова __async и __await в том виде, в каком они будут добавлены в Visual Studio.

Давайте вернёмся к реальному коду. Вашей работой часто будет его поддержка, даже если его написал кто-то другой. Представьте себе цепочку из std::future, auto и .then выражений, с вкраплениями лямбда функций - скорее всего это не то, на что вам хотелось бы смотреть каждый день. Но именно этим всё и закончится без resumable функций. Этот код не будет менее производительным, но вот время, которое вы потратите на его модификацию будет значительно больше, чем в случае с простыми и логичными async/await. С использованием resumable функций компилятор берёт на себя массу отвлекающих деталей, заботится о корректности "границ" областей видимости, правильно заворачивает результаты вызовов асинхронных функций в std::future, так что на данный момент счёт как минимум 1:0 в пользу resumable функций.

Идём дальше. Мы уже выяснили, что resumable функции добаляют изящества в код и делают вещи более простыми. Но достаточно ли этого, чтобы вот прямо взять и изменить язык С++? Вряд ли. Должны быть и другие причины. И они есть. Смотрите - у нас есть 2 варианта: поддержка на уровне библиотек и поддержка на уровне языка. Если забыть о красоте синтаксиса - равнозначны ли они в остальном? Нет. Давайте представим себе, каким образом может происходить отладка асинхронного кода. В случае использования библиотечного решения вопрос отладки стоит очень остро. Вы пробовали отлаживать чужие библиотеки? Закрытые библиотеки? Даже в случае открытой библиотеки мы будем постоянно терять контекст при переходам по цепочке futures (стек-то у каждой функции свой). И не факт, что при возникновении ошибки мы будет в состоянии понять, как сюда попали, кто нас вызвал и почему. В случае же поддержки resumable функций на уровне языка все инструменты (компилятор, отладчик) так или иначе будут работать "в одной связке": компилятор может сгенерировать код, удобный для отладчика, у отладчика не будет варианта НЕ дать нам нужного для отладки функционала, он не будет ссылаться на написанный не нами код - мы просто получим всё необходимое "из коробки".

Как я уже писал ранее, resumable функции в некоторой степени ограничены. Они могут возвращать лишь std::(shared_)future или void. Это не лучший вариант, было бы удобно иметь возможность вернуть boost::future или hpx::future. Возможно, этого удастся достичь через применения "концептов", но пока что есть как есть. Второе ограничение - resumable функции не могут использовать VArgs, для этого придётся написать отдельную функцию-обёртку. И я пока не очень понимаю, относится ли это ограничение к variadic templates. Ну и кроме того, на значение, возвращаемое resumable функцией накладываются ограничения, которые существуют для типов, которые можно использовать в std::future - на практике это означает обязательное наличие copy/move конструкторов.

Планы на будущее

Как я писал ранее, эта фича не войдет в С++14. Она была бы просто киллер-фичей, но к сожалению (к счастью?) С++14 по определению не должен (и не будет) содержать киллер-фич на уровне языка. Его задача - исправить явные баги С++11 и заложить основу для масштабных улучшений в будущем. Так что всё, о чём мы здесь говорили - удел С++y1. Следующий большой шаг для resumable функций - стать частью технической спецификации (TS), ответственная за это подгруппа в комитете - WG21. Несмотря на то, что синтаксис и ограничения resumable функций понять достаточно легко, реализация в компиляторе простой не является. Есть несколько вариантов реализации и пока нет согласованного взгляда, какой из них является лучшим. Как я уже говорил, первая реализация войдет в CTP к Visual Studio в конце этого года (прим.переводчика: и она вышла!). Эта реализация будет использовать ключевые слова __async и __await.

Важно понять, что всё написанное в статье ещё находится в работе. Многое зависит от утверждения в С++14 спецификации на .then() для future, await в итоге может быть даже построен на std::future::get.

Мнение

Пару слов с моей колокольни. Я вообще не очень-то по всей этой асинхронности и параллелизму, есть ребята поумнее меня - им и решать. Мне нравятся resumable функции в предложенном варианте. Также неплохим является вариант, предложенный в Cilk (уже есть реализация и она работает - проверено). Сравнивая их - resumable функции всё-равно кажутся мне чуть более элегантными - кода меньше, а возможности те же.

Новые ключевые слова могут поломать существующий код (а вдруг у вас были переменные с именами __async\__await ?). Авторы предложения resumable функций проанализировали STL и Boost и не нашли ничего подобного - уже неплохо.

Немного меня смущает то, что resumable функции - неполноценные функции из-за своих ограничений (см. выше). Но, наверное, они и не должны ими быть. В конце-концов, вы ведь не собираетесь использовать их вот прямо везде? Это лишь один из удобных механизмов асинхронности, такой себе синтаксический сахар, иногда полезный, а иногда - нет.

Кроме того, есть целый зоопарк вопросов, связанный с деталями реализации resumable функций. Например - должны ли существовать resumable lambda functions ? Если вы заинтересовались вопросом - почитайте заметки группы WG21, начиная с июля 2013 года.

Обновление - документ N3722

В конце Августа был опубликован документ с обновлением предложения resumable функций. Первое изменение состоит в том, что ключевое слово async заменено на resumable. Это хорошо, поскольку resumable функции теперь называются "resumable", что в общем-то логично. Значение ключевого слова await не изменилось.

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

  • метод get(), который возвращает тип Т или генерирует исключение
  • метод then(), который получает callable object с параметром типа s, s& или const s. Значение этого параметра должно быть сразу же доступно методу get().
    опциональный метод is_ready(), возвращающий состояние future


    Далее авторы рассуждают о том, что должен быть определён тип
    s::promise_type, который будет доступен реализации resumable функции. Этот тип должен предоставлять методы set_value(T) и set_exception(exception_ptr). Должно существовать неявное преобразование между s::promise_type и s.

    Генераторы

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

    Для достижения этого эффекта предлагается ввести новое ключевое слово yield:

    sequence<int> range(int low, int high) resumable { for(int i = low; i <= high; ++i) { yield i; } }


    yield будет вычислять значение i когда это понадобится при запросе к sequence. Каждая итерация по sequence будет вызывать функцию, ожидая получить следующий результат от yield. Здесь не идёт речь о многопоточном или асинхронном программировании - всё выполняется в одном потоке, но только лишь тогда, когда понадобится. Документ предлагает комбинировать в коде yield и await для достижения нужных программисту результатов.
Метки:
Инфопульс Украина 102,61
Creating Value, Delivering Excellence
Поделиться публикацией
Комментарии 65
  • +30
    Картинка с чуваком которого выкидывают из окна:
    — Итак, какой сделаем новый сервис пак?
    — Поддержка COBOL!
    — Новые фичи из С++17!
    — А может быть более полная поддержка С++11 в 2010 и 2013 студии?
    * выкидывание из окна *
    • +6
      :)
      Да ладно, посмотрите список остальных фич — там десяток фиксов именно для С++11.
      • 0
        Когда constexpr то сделают? И инициализацию полей в классах.
        • +3
          так в 13 уже есть инициализация полей, а с обновлением который упомянут в начале появятся и constexpr.
          • +6
            Ну вот же из анонса:
            "-constexpr (note: except for constructors, so the CTP won’t support literal types yet)"
      • –4
        MS молодцы, все это давно уже есть в шарпе, читая все это возникает дежавю.
        • +9
          Это уже 40 лет есть в Smalltalk. Возникает дежавю.
          • +1
            Самое интересное, что в самом Smalltalk-е этого нет. Но есть в библиотеке. На самом деле, есть континуации — и тоже не в языке, в библиотеке. А через континуации легко делаются сопрограммы. (Впрочем, можно и без континуаций — через процессы.)

            Так вот, на что хочу обратить внимание:, когда кому-то понадобились континуации, он просто их взял и сделал сам — без внесений изменений в язык! Причем сделал все это, уйдя с Ruby, так как не смог на тот момент добиться внесения тех самых континуаций в язык — реализовать их как библиотеку было никак (не знаю, что-то изменилось с тех пор? было это уже лет 10 если не 15 назад). В общем, выводы делайте сами… :)
            • +1
              Ну это само собой, на то он и Smalltalk. А так, что континуации, что транзакционная память, что ленивые вычисления — реализуются одинаково легко, буквально в 50 строк, чем мне это и нравится.

              P.S.: Уже практически написаны и скоро выйдут еще 2 мои статьи по сабжу :)
            • +2
              Со Smalltalk, увы, не доводилось работать. Если это настолько мощный язык — почему он не в числе основных, используемых для разработки бизнес-приложений? Просто любопытно.
              • +1
                Он использовался и до сих пор используется в областях, где необходимо в одной системе объединить огромное количество бизнес логики. Причем, наращивание объемов и функциональности системы, на удивление, не приводит к усложнению кода. Грубо говоря, сложность восприятия растет линейно в зависимости от объемов кодовой базы.

                В свое время Smalltalk был вытеснен Java и, как многие считают, это вытеснение было целенаправленным и, по сути своей, политическим решением.

                Вообще, если интересно, можете посмотреть лекцию на youtube, где Роберт Мартин рассказывает о том, почему Smalltalk ушел со сцены, и предостерегает сообщество Ruby (во многом, идейного наследника Smalltalk) от подобной участи.
                • 0
                  … Только, пожалуйста, не воспринимайте то, что дядя Боб там говорит очень всерьез. Он извращает ситуацию в угоду развлекательности и продвижению «своих» «идей» о программировании (первое подчинено второму, разумеется).

                  Ситуация со Smalltalk довольно сложна, и там всего намешано. Конечно, Smalltalk не идеален, но он менее неидеален, чем современный mainstream. Причина его провала не в этом. Да и о полном и окончательном провале говорить пока что рано — «я так думаю!»). Идеи, заложенные в Smalltalk, пробираются к людям разными путями, иногда очень медленно, и почти всегда сначала в очень перевранном виде. Но находят. Так что, вполне вероятно, мы еще встретимся со Smalltalk либо непосредственно, либо в новом воплощении. … В общем, закругляюсь, ибо оффтопик получается.
                  • 0
                    Да понятно. Тем не менее, это тоже способ обратить внимание ответственности на это дело. По крайней мере, те моменты, которые он упоминает, достойны внимания.
          • 0
            А можете пояснить, почему конструкцию с await/async называют resumable? Там же ничего не приостанавливается, нет?

            А есть какая-то информация на тему того, как будет работать yield? Что будет происходить с контекстом текущего треда?

            Как это вообще будет внутри работать?
            • 0
              Я подозреваю что yield будет работать как в C# но немного иначе, что-то вроде

              auto seq = range(0,100);
              do
              {
              int i = seq.next();
              // do smth with i
              }while(i < 100);
              • +2
                Дело в том, что C# (как и питон) — управляемый язык. На нативном C++ реализация будет сложнее.
                • +4
                  Почему? yield в C# реализуется очень просто: для метода создается класс, в котором есть поля для всех локальных переменных, а также поля StateId и Current. В начало кода метода вставляется блок-диспетчер наподобие следующего:

                  switch(StateId)
                  {
                      case 1: goto YIELD_1; break;
                      case 2: goto YIELD_2; break;
                      ...
                  }
                  

                  Не вижу никаких причин, почему аналогичную конструкцию сложно будет реализовать на неуправляемом языке.
                  • +1
                    Более того, есть же реализации на уровне boost'а.
                    www.crystalclearsoftware.com/soc/coroutine/coroutine/tutorial.html#coroutine.generators
                    • +2
                      А что происходит с регистрами? А со стеком?
                      А я правильно понимаю, что у нас внезапно все переменные вместо стека начинают в куче выделяться?
                      • +1
                        Не все, а только те, которые в resumable-функции. Это один из вариантов реализации, как писалось в статье, пока не понятно, стоит ли реализовывать этот так, или придумывать что-то еще.
                        • +4
                          Я бы воспринимал yield скорее как синтаксический сахар.
                          Представьте, что есть следующий код: (пример на C#, но думаю человеку знакомому с C++ будет достаточно понятно)

                          public IEnumerable<int> Enumerate()
                          {
                          	yield return 1;
                          	yield return 2;
                          }
                          

                          Он трансформируется в достаточно длинную портянку:

                          public IEnumerable<int> Enumerate()
                          {
                          	return new __InnerClass1();
                          }
                          
                          class __InnerClass1 : IEnumerable<int>
                          {
                          	public int Current { get; private set; }
                          	private int StateId;
                          	
                          	public bool MoveNext()
                          	{
                          		switch(StateId)
                          		{
                          			case 0: goto YIELD_1; break;
                          			case 1: goto YIELD_2; break;
                          			default: return false;
                          		}
                          		
                          		YIELD_1:
                          		Current = 1;
                          		StateId = 1;
                          		return true;
                          		
                          		YIELD_2:
                          		Current = 2;
                          		StateId = 2;
                          		return false;
                          	}
                          }
                          

                          Все локальные переменные, требуемые между вызовами yield, должны быть реализованы в виде полей класса __InnerClass1, а соответственно да, скорее всего будут выделяться в куче, а не на стеке. Разумеется, на скорости выполнения это сказывается негативно — уже хотя бы потому, что каждый yield вызывает переключение контекста. С другой стороны, итераторы экономят память: с их помощью можно описывать бесконечные последовательности значений, или просто писать понятный и читаемый код, не критичный к скорости выполнения.
                          • 0
                            new __InnerClass1 будет создан в куче со всеми вытекающими — нехватка памяти, исключения, необходимость следить за исключениями и прочее.
                            • 0
                              Это кстати немного напрягает, что появляются такие неявности запрятанные за обычным кодом. Еще немного непонятно насколько все это действительно будет работать на практике (не пришлось использовать async/await C#), в часности интересно такая вещь: если мы вызываем async функцию внутри своей, свою функцию надо тоже объявить как async (либо заблокироваться и ждать окончания). Получается async «заражает» всю цепочку вызовов наверх.
                              • +1
                                Не обязательно. Компилятор может объявить его как struct и объявлять в качестве локальной переменной на стеке в том методе, где был вызван итератор, тем самым заинлайнив его. Вариант нехватки памяти на объект такого типа кажется мне примерно настолько же вероятным, как ситуация, когда у вас вдруг переполнится стек при попытке аллоцировать на нем локальную переменную.

                                Опять же, серебряной пули не бывает. yield и async-await не вносят в язык ничего такого, что раньше вообще было невозможно реализовать. Они просто добавляют удобный и легковесный синтаксис, за который, как за любую обертку высокого уровня, придется платить производительностью. Никто не заставляет вас использовать эти возможности в вашем коде, если вы выжимаете из него последние байты и миллисекунды, но появление таких возможностей в языке — это всегда приятно.
                    • 0
                      Наверное потому, что оригинальная функция прекращает свою работу, «подписывая» future на некую лямбду с остальным кодом. Он уже будет вызван другой стороной по мере появления значения и тем самым функция «продолжится».
                      • +1
                        resumable — это ведь не «приостановка», это «возобновление». Работа функции, дёрнувшей через await другую функцию, возобновляется после выполнения этой другой функции.
                        • +1
                          1. Можно войти в тот же самый стек фрейм (не путать с другим стек фреймом для той же самой функции) даже после выхода из этой функции
                          2. Точно так же как и __async/__await, собственно yield довольно тривиально пишется если есть __await (но сахар все равно лучше)
                          3. Возможных реализаций несколько и одна из них — та, что в шарпе — конечный автомат. На build2013 (кажется, хотя может это был и going native) был доклад о реализации __async/__await в VS. Для каждой операции, выполняющейся асинхронно, динамически выделяется отдельный стек фрейм и указатель стека перекидывается между фреймами в прологе/эпилоге функции.
                        • +2
                          Почему-то думал, что будет говорится как раз о корутинах и yeld, а последний здесь упомянут, но не в контексте сопроцедур, а на них же тоже можно строить вполне удобную замену колбэкам.
                          • +2
                            К слову о плюшках в свежих плюсовых компиляторах: недавно попробовал сделать массив из функций с помощью списка инициализации в ICC. Вот результат. Я немного в шоке :)
                            • 0
                              Ну, насколько я понимаю, лямбда имеет другой тип, нежели функция с такой же сигнатурой. Но AV — это конечно, слишком.
                              • 0
                                Это ещё хорошо, что AV вылезло. А это ведь значит, что там совсем неопределённое поведение, оно могло и скомпилироваться, а вылезти уже потом в программе случайным образом.
                                • +1
                                  Да, но в MSVC почему-то работает на ура.
                              • +2
                                Интересно, а кого они троллят, вводя в VS 2012 в диалекте C++/CLI такой синтаксис?
                                public ref class FooBase abstract : public BarBase {
                                
                                ..
                                		virtual void GetFoo(Platform::WriteOnlyArray<int>^ params);
                                internal:
                                		void Bar(Platform::Object^ sender) override
                                		{
                                				Window::Current->CoreWindow->SizeChanged += 
                                					ref new TypedEventHandler<CoreWindow^, WindowSizeChangedEventArgs^>(sender, &FooBase::OnWindowSizeChanged);
                                		}
                                ..
                                };


                                C# программисты в шоке, С++ программисты морщатся в отвращении. Смысл, конечно, понятен, но мне жалко тех, кому прийдется такой «С++» код саппортить или даже портировать на другую платформу.
                                • +6
                                  И мне жалко. Но вообще весь С++/CLI — это либо чтобы сделать тонкий клиент от С++ кода к дотнету (если из него чего вдруг надо), или тонкая прослойка между основным продуктом на .NET и проверенной библиотекой на С++, которую переписывать нет ни желания ни нужды.

                                  В пределах пары сотен строк с С++/CLI ещё можно жить. Писать на нём целиком большой продукт — это надо быть хорошо двинутым.
                                  • 0
                                    Я на нем писал курсовые в 2007..2008 годах, когда препод требовал «обязательно на С++», но хотелось функционала .NET :)
                                    В целом — терпимо, но на тот момент я еще не знал C#. После него смотреть назад на C++/CLI вообще невозможно.
                                  • 0
                                    Это C++/CX. Я только понять не могу чего C++ программисты морщатся? Делегаты сам приводить он не умеет, потому такая страшная строчка в Bar() вышла. В остальном все довольно неплохо.
                                  • +1
                                    Имхо вполне правильно тащить удачные фичи из одного языка в другой.

                                    В C++ обязательно надо добавить async\await и yield из C#. А в C# надо реализовать шаблоны, как в C++ или хотябы подмножество, как в F#.

                                    А еще в обоих надо TypeClasses как в Цацкелле.
                                    • +1
                                      А в C# надо реализовать шаблоны, как в C++

                                      Странно. Я всегда считал, что в С++ самые ужасные шаблоны.
                                      • +3
                                        Они ужасны, но полны по Тьюрингу, поэтому на них можно реализовать хоть чёрта лысого.
                                        • +1
                                          Лучше постшарп прикрутить в таком случае
                                          • 0
                                            Полны, да не совсем, ибо компиляторы накладывают ограничения на глубину вычисления шаблонов. Так что чёрт лысый должен быть относительно просто устроен.
                                          • +1
                                            По сравнению с чем?
                                            • +2
                                              самые ужасные шаблоны
                                              Очевидно, по сравнению со всеми остальными языками.
                                              • 0
                                                По сравнению с C#, например.
                                                • +3
                                                  В C# нет шаблонов, есть Generic. Шаблоны инстанцируются в compile-time и могут, например, реализовать rank-2 полиморфизм.
                                                  А F# есть подобная штука, называется compile-time generics, но она работает только в inline функциях, что сильно ограничивает применение.
                                                  • +1
                                                    Я понимаю, что шаблоны и generics — это не одно и то же. Generics из C# красиво и элегантно вписан в систему типов, а шаблон в C++ — это тупо макрос, который тупо подставляет типы в нужные места, а потом пытается скомпилить.
                                                    Я понимаю, что макросы — это тоже хорошо. Местами, они бывают незаменимы. Но я бы предпочел язык с мощной системой типов, языку с мощной системой макросов.
                                                    • 0
                                                      А почему нельзя совместить? Что принципиально мешает сделать compile-time generics в C#? Что мешает сделать явные ограничения для шаблонов в C++?
                                                      • 0
                                                        Да ничто не мешает. Теоретически могло быть и то, и то.
                                                        • 0
                                                          Более того, concepts в C++ (и его новая инкарнация — concepts lite) именно об этом.

                                                          • 0
                                                            О, они сделали лайт-версию! Спасибо, не знал. Сразу было понятно, что предыдущая не взлетит.
                                                      • 0
                                                        Как минимум, generics в C# не поддерживает ни явную, ни частичную специализацию, ни переменное число параметров (variadic generics), а также не позволяет задавать типы в качестве параметров по-уолчанию и определять нешаблонные типы.
                                              • 0
                                                std::yield реализовать стоит — его реализация в духе недавних добавлений функциональщины. А зачем await? Какая-то нехорошая идея у [Microsoft?] плодить кучу отдельных слов (async хотя бы стоит в определённом месте и его можно не делать зарезервированных в других местах), хотя всё можно прекрасно запихнуть в методы std::futures.
                                                И я так и не понял, зачем нужны асинхронные не системные вызовы? Они ж всё равно не смогут выполняться в то же время (а неявный вызов других потоков — это слишком высокоуровнево для включения в C++).
                                              • 0
                                                Кстати вопрос знатокам.
                                                Если я правильно понимаю, то работать async\await будет ровно как в C# — переписывать функции в КА, а замыкания в классы. Как при этом будет сделан контроль времени жизни\владения? Где будет храниться объект-замыкание?
                                                • 0
                                                  Это какой-то не С++-способ, оверхеда много. Скорее сделают выделение области данных в куче, туда поскладывают всё нужное, а контроль времени жизни этой области сделают каким-нибудь счетчиком ссылок на неё из всех замыканий. Но это так, фантазии. Посмотрим, как оно будет.
                                                  • +1
                                                    И что там с исключениями?

                                                    Ты, кстати, проходишь курс Мартина Одерского и Эрика Мейера «Principles of Reactive Programming» на курсере? В Scala те же async/await.
                                                    • 0
                                                      Я записался, но проходить времени нет. Может нагоню еще… (мечты-мечты)
                                                  • 0
                                                    Об этой теме неплохо рассказали на GoingNative
                                                    channel9.msdn.com/Events/GoingNative/2013/Bringing-await-to-Cpp
                                                    Из доклада становится ясно, что Microsoft включи эту фичу в компилятор по своим собственным соображениям, скорее всего ради пользователей WinRT.

                                                    Вообще какая-то странная статья.

                                                    std::future<int> f_int = make_dummy_future(42);
                                                    int i = f_int.get() // ждём окончания работы функции
                                                    f_int.then([](std::future<int> i){/* deal with it */})
                                                    

                                                    Крайне сомнительное использование then. Зачем он, если результата мы уже синхронно дождались строчкой выше?

                                                    а вдруг у вас были переменные с именами __async\__await ?

                                                    ССЗБ, стандарт резервирует такие имена.
                                                    • 0
                                                      >Зачем он, если результата мы уже синхронно дождались строчкой выше?
                                                      Это два несвязанных друг с другом примера использования. Я согласен, что немного нелогично выглядит, но так в оригинале, а это всего лишь перевод.

                                                      >ССЗБ, стандарт резервирует такие имена.
                                                      Вон в предложении уже решили __async в resumable переименовать. Риск некоторый есть.
                                                      • 0
                                                        решили __async в resumable переименовать

                                                        Я где-то слышал (кажется, в Design and Evolution of C++), что новые ключевые слова, добавляемые в язык, выбираются странноватыми как раз для уменьшения вероятности коллизий. typename, к примеру странное слово, и обычный программист врядли решит так назвать класс или перменную. Думаю, с resumable аналогичная ситуация. Мне бы такое слово в голову само не пришло.
                                                        • 0
                                                          В предложении __async не было, было async.

                                                          Для правильного кода риска нет — по стандарту все имена, начинающиеся с двух подчеркиваний, зарезервированы для реализации языка (в т.ч. для языковых расширений). Именно отсюда все эти __async и __attribute__.
                                                      • 0
                                                        А потом комиссия по языку составит свой вариант данного решения и будет два несовместимых варианта реализации: MSVC и все остальные. Что-то мне это напоминает.
                                                        • +4
                                                          Херб Саттер (из Майкрософта по С++ который) — один из главных идеологов С++11, общих стандартов, большой друг Страуструпа и вообще мужик со здравым смыслом в голове. Вряд ли будет два несовместимых стандарта.
                                                        • +2
                                                          В оригинальной статье многое, увы, напутано. Причем это ровно те же грабли, на которые люди массово наступали, осваивая async/await в C#.

                                                          >> Внутри resumable функции вещи происходят тоже слегка иначе. Используя ключевое слово await теперь можно «завернуть» выражение или вызов функции в future, которая посчитает это выражение или вызов в другом потоке.

                                                          await ничего никуда не заворачивает — наоборот, она «разворачивает» значение типа std::future (возможно, но не обязательно, возвращенное из другой resumable функции), асинхронно ожидая завершения вычисления и вытаскивая полученное значение (или исключение).

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

                                                          >> И вот мы подоходим к интересному. Вы можете использовать ключевое слово await неоднократно — каждое его применение создаст std::future, которая начнёт выполняться в параллельном потоке.

                                                          Те же грабли, вид сбоку. Еще раз — await не создает экземпляры std::future, а получает значения из них.

                                                          Ну и о «параллельных потоках» — это тоже неправда. Далеко не каждый future выполняется на своем потоке. Например, вполне возможна реализация всяческой асинхронщины в reactor style, вообще на одном потоке (как в node.js) с циклом сообщений/коллбэков. Туда же всяческие completion ports и прочие средства асинхронного I/O.

                                                          Единственный случай, когда future гарантированно выполняется на своем потоке — это когда синхронный код обернут через std::async. Во всех остальных случаях, это деталь реализации данного конкретного future.
                                                          • +2
                                                            На rsdn критикуют: rsdn.ru/forum/cpp/5366756.1 и rsdn.ru/forum/cpp/5366761.1
                                                            весьма любопытно почитать — получается, что данное решение собирает недостатки stackless и stackfull coroutines.
                                                            • 0
                                                              Небольшое замечание про «обновление компилятора — нетривиальное событие»
                                                              Вообще-то был «November 2012 CTP», (ну правда он был доступен уже в октябре ;-) )
                                                              В нем впервые появились многие фичи C++ 11.

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

                                                              Самое читаемое