Search
Write a publication
Pull to refresh
12
0
Ахметзянов Денис @axden

Разработчик C/C++/Rust

Send message
Интересно, что Google применил аналогичный подход thread-per-request при разработке Google Percolator, системы инкрементного обновления индекса (вместо Map-Reduce-based).
Описано здесь. В качестве плюсов они приводят:
— код проще
— хорошая утилизация CPU на многоядерных машинах
— легче читать stack trace'ы
— гонок в коде оказалось «меньше, чем опасались»

Самое интересное, что для решения проблем с масштабируемостью и большим числом потоков, они специально пропатчили Linux ядра на своих серверах. И видимо удалось как-то сгладить проблемы. Жаль подробности не приводятся.

Такое получилось вынесение сложности из Application кода в kernel.

Полная цитата из документа:
Early in the implementation of Percolator, we decided to make all API calls blocking and rely on running thousands of threads per machine to provide enough parallelism to maintain good CPU utilization. We chose this thread-per-request model mainly to make application code easier to write, compared to the event-driven model. Forcing users to bundle up their state each of the (many) times they fetched a data item from the table would have made application development much more difficult. Our experience with thread-per-request was, on the whole, positive: application code is simple, we achieve good utilization on many-core machines, and crash debugging is simplified by meaningful and complete stack traces. We encountered fewer race conditions in application code than we feared. The biggest drawbacks of the approach were scalability issues in the Linux kernel and Google infrastructure related to high thread counts.

Our in-house kernel development team was able to deploy fixes to address the kernel issues.
Но обсуждается-то не модель с её «принципиальными недостатками», а конкретное предложение!

Согласен, возможно слишком увлекся сравнением абстрактных моделей Suspend-up vs Suspend-down, поэтому в тот момент мне показалось, что раз проблемы со scheduling'ом и starvation устранимы (используя yield, spawn, изменением реализации await, как угодно), то это и несущественно.

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

Тут не уверен.
Если мы говорим про критику из "Coroutines belong in a TS" в целом, то модель suspend-up c await как была инвазивная так и осталась. А это было основным замечанием.

Если говорить про пункт "2.8.2 Security and Performance Risks", который в статье упомянут как

5) Риск для безопасности и производительности: текущий дизайн сопрограмм увеличивает риск jitter'a, недостатка ресурсов отдельным задачам (starvation) и DOS атаки.

то причиной этого авторы "Coroutines belong in a TS" называют реализацию await:

 ( await-ready-expr
     ? await-resume-expr
     : (await-suspend-expr, suspend-resume-point, await-resume-expr))

В том случае если выражение await-ready-expr все время готово (как генератор случайных чисел выше), то сопрограмма будет все время продолжаться и продолжаться, захватывая процессорное время. Цитата:

this essentially means that if the Awaitable is already ready (await_ready returns true) the coroutine continues without suspending. And so on and so on for an indefinite number of iterations. This can at best result in high jitter; and at worst starvation.

В новой редакции P0057R2 от 12 февраля 2016 я вижу, что выполнение также будет продолжено, если awaited-выражение готово:

The await-expression evaluates the await-ready expression, then:
— If the result is true, or when the coroutine is resumed, the await-resume expression is evaluated, and its result is the result of the await-expression.

Не увидел разницы.

Если исправление в том, что добавлено слово co_yield и его можно вставить в цикл:

Значит, надо всего лишь добавить задержку. Или вставить в цикл инструкцию принудительного переключения контекста (в js такое делается согласно стандарту Promises/A+ для любого продолжения, в C# это делается через await Task.Yield() — значит и для C++ решение найдется)

то это не совсем то, чего требуют авторы "Coroutines belong in a TS". Они настаивают, что сопрограммы должны быть спроектированы таким образом, чтобы не перекладывать эту заботу на пользователя сопрограмм и starvation был исключен by-design. Предлагается запуск с указанием характеристик выполнения:

Scheduling ought to be a cross cutting concern for the coroutine, so we'd much rather see it as a library interface, something like:

    spawn(always_suspend, some_async_function);

or:

    spawn(always_continue, some_async_function);

spawn, насколько я понимаю, что-то похожее на spawn из Boost

В еще более позднем proposal "A networking library extension to support co_await-based coroutines" от 14 февраля 2016 Christopher Kohlhoff более подробно описывает работу spawn с co_await:

New coroutine-based threads of execution are explicitly launched using a spawn function. This function also allows the user to specify the execution properties of the new thread of execution.

5.2. Introducing new threads of execution should be explicit

As mentioned above, coordinating multiple threads of execution is a requirement of all but the most trivial applications. Even if a networking application is single-threaded, there still exists concurrency in the scheduling and execution of these threads of execution. Therefore, to reduce the risk of programmer error, the introduction of new threads of execution should be explicit.

In this proposal, new coroutine-based threads of execution are initiated using the spawn function. In addition to launching a new thread of execution, this function requires the programmer to specify the executor that will be used for it.

Ничего такого в последней ревизии P0057R2 не видно. Поэтому это замечание, в той форме в какой было сформулировано, также не исправлено.
Хотя средства, чтобы обходить проблему, в виде co_yield есть, это да.
я думал, тут его перевод-пересказ опубликован

Как я отмечал во вступлении, эта статья скорее дайджест из исходного документа. Цель была привлечь внимание к альтернативной точке зрения на сопрограммы с await (особенно suspend-up vs suspend-down).

До этого я смотрел доклад "C++ Coroutines — a negative overhead abstraction" и ответы Гора на Q&A в Яндексе, там минусы модели suspend-up особо не упоминались. Поэтому для меня точка зрения, представленная в "Coroutines belong in a TS", выглядела как новый и неожиданный взгляд на await. Поэтому решил поделиться и услышать мнение знающих людей.

Сожалею, если сбило с толку, добавил UPD2 с пояснением в текст.
В июле 2015 на C++ User Group Meeting был доклад Александра Фокина из Яндекс на тему текущего состояния "Resumable функции в C++".
Там он сообщает, что "честной" реализации coroutines в Visual Studio 2015 RC нет, а сделано на fibers.

С 14-й минуты видео со слов "Теперь плохие новости...".

К 2017 году MS может и запилят "честную" реализацию без Thread Pool и fibers. Хотя для реализации в GCC и Clang потребуется еще время и испытания могут вылететь из графика C++17.
А где вы смотрите? Видео с заседания где-то выкладывают?
Лично для меня проблема с STL алгоритмами не стоит) Я бы и "рукопашные" циклы писал, чтобы пользоваться await вместо callback'ов. Поскольку сallback'и в наших асинхронных операциях значительно запутывают код, c await было бы проще.
Я скорее рассматриваю аргументы Кристофера.

Могу предположить, что это может быть полезно, когда для каждого элемента последовательности нужно вычислять предикат настолько дорогой/долгий, что его выполнение выносится в фон. std::any_of, std::find_if, std::count_if, remove_if. То есть запустили вычисление предиката для первого элемента в фоне, вернулись в EventLoop, когда досчитали, "проснулись" с await, перешли к следующему элементу последовательности и снова запустили предикат в фоне.
Да, конечно, проблемы возникнут только при вызове асинхронного кода из алгоритмов STL. В своей работе «Resumable Expression» Christopher Kohlhoff более подробно описывает проблему с STL. Он называет это "Острова абстракций".

Такой код работать не будет:

std::future<void>   tcp_sender(std::vector<std:string>  msgs)
{
        auto    conn    = await Tcp::Connect("127.0.0.1”,   1337);
        std::for_each(msgs.begin(), msgs.end(),
                [&conn](const   std::string&    msg)
                {
                        await   conn.write(msg.data(),  msg.size());
                });
}

Поэтому придется писать новую вариацию алгоритма, который знает про await:

template    <class  I,  class   F>
std::future<void>   resumable_for_each(I    begin,  I   end,    F   f)
{
        for (I  iter    =   begin;  iter    !=  end;    ++iter)
                await f(*iter);
}

std::future<void>   tcp_sender(std::vector<std:string>  msgs)
{
        auto    conn    = await Tcp::Connect("127.0.0.1”,   1337);
        resumable_for_each(msgs.begin(),    msgs.end(),
                [&conn](const   std::string&    msg)
                {
                        await conn.write(msg.data(),    msg.size());
                });
}

Со временем это может привести к появлению набора алгоритмов, которые являются "отражением STL". Это будут "острова" по-мнению Кристофера.
Также небезинтересное обсуждение документа "Coroutines belong in a TS" на reddit и здесь. Мнения разделились. Некоторые считают await неприемлемым, потому что корутины "non-composable", плохо сочетаются с синхронным кодом и STL.

Другой пользователь возражает, что критика await имеет несколько FUD стиль (Fear, uncertainty and doubt), мало конкретики и неопределенные страхи. А явное указание await он считает хорошим подходом.

Information

Rating
Does not participate
Location
Москва, Москва и Московская обл., Россия
Date of birth
Registered
Activity