Обновить
129
0
Григорий Демченко@gridem

Software Engineer

Отправить сообщение

Ок, тогда просто прошу реализовать это:


// Вычисление (2v+1)^2
Future<int> anotherValue = value
    .Then([] (int v) { return 2 * v; });

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

Основная идея future/promise — это композиция вычислений. Мне очень интересно, как с помощью этого интерфейса можно реализовать что-то типа этого:


// Вычисление (2v+1)^2
Future<int> anotherValue = value
    .Then([] (int v) { return 2 * v; })
    .Then([] (int u) { return u + 1; })
    .Then([] (int w) { return w * w; });

Конкретно CoroutineTS основан на специальных объектах, которые очень похоже на future/promise, но при детальном рассмотрении — совсем не похожи.


Для этого рассмотрим интерфейсы:


template <typename T>
struct my_future {
    bool await_ready();
    void await_suspend(std::experimental::coroutine_handle<>);
    T await_resume();
};

Легко видеть, что тут нет никаких then, onError и т.д. Т.е. это похоже на future/promise примерно так же, как Java похожа на Python.

> А ничего, что сопрограммы основаны на «future и promises»?

Вообще говоря, сопрограммы и future/promise — вещи ортогональные. Можно реализовать сопрограммы без использования future/promise подхода (см. synca: habr.com/ru/post/201826), обратное тоже верно. Можно их скомбинировать и получить сборную солянку, как в вышеприведенной статье.

Однако, как мне кажется, такая солянка ясности это не добавляет. Даже наоборот.
> Я выбираю «корутины», потому что это глобальная международная терминология

Терминология может быть международной, но это слово вполне себе переводится. Корутины — так, конечно, можно говорить, но это жаргонизм, примерно как «шедулить» вместо «планировать», «заводить баги» вместо «создавать задачи/дефекты». Т.е. понять могут, но по-русски это несколько безграмотно.
Сопрограммы позволяют решить проблемы и задачи там, где нужна производительность и простота кода. Многопоточность нужна для эффективности. Если эта эффективность и производительность не нужна, то тогда и остальное не нужно, включая сопрограммы.

А какой вариант предлагается вместо этого? Модель fork — можно пояснить, что это за модель?
Вот здесь показан один из подходов, как можно обрабатывать таймауты легко и непринужденно: habr.com/ru/company/yandex/blog/240525

Вот здесь рассмотрен пример с таймаутами и нетривиальной параллельной обработкой: https://habr.com/ru/company/yandex/blog/240525/

Кстати, synca::Mutex гарантирует FIFO, т.е. является честным, а значит будет удовлетворять требованию задачи.

Вот как раз интересно и будет сравнить подход на потоках, акторах, сопрограммах, CSP и проч. Мне кажется, сопрограммы по читабельности выиграют. При этом приведенный выше код легко переписывается на сопрограммы заменой std::mutex на synca::Mutex из https://habr.com/ru/post/340732/

Да, в решении Дейкстры подразумевается, что мьютексы честные, т.е. если мьютекс А захватил, затем мьютекс Б, то сначала его захватывает А, а затем Б. Стандартная реализация std::mutex этого не гарантирует. Но такой честный мьютекс можно сделать и самому, на основе обычного мьютекса и cond var.

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

«Устаревшие» обедающие философы на C++ без всяких извращений:


struct Fork {
    void take() {
        mut_.lock();
    }

    void put() {
        mut_.unlock();
    }

private:
    std::mutex mut_;
};

struct Philosopher {
    // Алгоритм Дейкстры
    void eat() {
        left_ > right_
            ? doEat(left_, right_)
            : doEat(right_, left_);
    }

private:
    void doEat(Fork* f1, Fork* f2) {
        f1->take();
        f2->take();
        // ням-ням
        f1->put(); // можно класть в любом порядке
        f2->put();
    }

    Fork* left_;
    Fork* right_;
};

Выводы очевидны.

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

Сюдя по написанному, именно так. Я не нашел в статье четкого и однозначного определения. Зато нашел другое:


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

Т.е. есть ощущения и неуверенности.

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


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


Важный аспект шардирования — это полная или частичная потеря консистентности. Поясню, что я имею в виду. Когда данные лежат на одном хосте в базе данных, то можно исполнять транзакционные запросы, которые будут обрабатывать изменения строк атомарно: либо все, либо ничего. Если же мы пошардируем, то при отсутствии распределенных транзакций свойство атомарности сразу теряется, если мы хотим выполнить кроссшардовую транзакцию. Поэтому мы либо платим потерей консистентности (для межшардовых операций), либо платим дополнительными задержками, которые необходимо добавить для распределенных транзакций. А задержки эти будут немалыми, если вообще кто-то это реализует. Ну и к тому же всякие join'ы придется реализовывать ручками, т.е. мы сразу откатываемся назад, как будто баз данных не существует.


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


Вот хотелось такого жира, а не толочь воду в ступе автором.

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

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


Проблема в том, что разработчики с++ заявляют: мы о вас подумали и вы платить не будете. А чтобы было веселее, мы добавили вам UB. Т.е. по факту:


  1. Либо есть плата в рантайме. Опять же, никто не измерял, все лишь говорят о ней. Предсказатель переходов для горячего кода должен вполне неплохо с этим справляться.
  2. Либо есть плата из-за того, что может быть UB. Т.е. корректность положена на плечи разработчиков. Это то, что должен каждый знать: С++ кладет на разработчиков. Прежде всего, кладет сложность, но и не только.

Мне всегда казалось, что вызывается std::terminate, т.е. никакого UB. Даже если UB, явного запрета нигде нет.

Пройдусь немножко:


1) по сигнатуре функции невозможно понять какое исключение может вылететь из функции.

Это проблема языка, а не исключений как таковых. Есть noexcept, которое говорит о том, что исключения не полетят.


2) размер бинарного файла увеличивается за счёт дополнительного кода поддержки исключений.

Да, это — плата, и это в некоторых случаях может стать решающим.


3) нельзя выбрасывать исключения из деструкторов.

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


4) долгая раскрутка стека и не соблюдение принципа «не плати за то что не используешь».

Если исключение не летит, то за это ты не платишь. Поэтому не очень понятно, что тут имеется в виду.


5) несовместимость с «чужими функциями».

Это я не очень понял. Можно раскрыть мысль?


6) нелокальнось (код обработки ошибки может быть далеко от того места где она произошла).

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


7) на некоторых платформах, например новом android ndk и clang нельзя полагаться на исключения между .so на версиях ниже 5.

Не является проблемой языка, и исключений в частности.


8) необходимо генерировать typeid и экспортировать его из .so для всех типов Увеличивает время линковки.

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


9) если не соблюдать RAII, то исключения легко ведут к утечке ресурсов, появляются неявные выходы из функций.

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


10) исключения тяжело внедрять постепенно в legacy код, который изначально не написан exception safety.

Это относится к любой вещи. Возможно, тут имелось в виду, что требования к качеству кода при работе с исключениями повышается. И нужно быть более внимательным. Да, это так. Это хорошо дисциплинирует. Однако это относится к любой вещи, которую хочется внедрить в существующую систему: часто требуется переписать огромные куски для интеграции. Поэтому, хоть это и является минусом, но не является чем-то уникальным для исключений.


В целом, у исключений есть минусы. Я бы не сказал, что их много, но они есть. Но и плюсов тоже немало, и часто они перевешивают минусы.

Информация

В рейтинге
Не участвует
Дата рождения
Зарегистрирован
Активность