LINUX.ORG.RU

Как понять асинхонщину в Rust?

 , ,


0

5

В Rust какая-то очень «самобытная» асинхронщина, понять её сложно. Итак, необходимый минимум - реализовать трейт Future. Дольше веселее - есть несколько каких-то малопонятных сущностей, которые за что-то там отвечают, пока непонятно, за что.

Читал кучу мануалов, они из серии: https://sun1-16.userapi.com/impg/cTOihOqKWsJSPD6uBUB6KTQZY823V6f3TzS9Tw/5VAn_b_1JUI.jpg?size=1080x1080&quality=96&proxy=1&sign=3ec0c8687655c3ac555d4b06874cfc49&type=album

Как правильно достать значение из Future? 101 вопрос. Когда Future реализуется сам, а когда его необходимо реализовать самому? Я так понял, логика реализуется в poll(), нет?

Мне нужно, например, читать стдаут порождённого процесса или поток миди-данных в рандомное время на вводе.

Кто распишет вменяемый хеллоуворлд, скину на пиво;)

★★★★★

Ответ на: комментарий от Siborgium

Сомнительно, что его можно восстановить. Ему уже жопа пришла. Ну там в ближайшее время, но писать ничего не обещаю. Мне практически всегда некогда. Опять зашёл на эту помойку. Но я не хочу на это тратить много времени.

anonymous
()
Ответ на: комментарий от cloun1901

tl;dr - это не zerocost(под те же фьючи уже нужно немножко но памяти и немножко логики чтобы поспать до получения результата) и не особо «дырявая»(например если задачи выполняются на пуле потоков, то редко когда имеется возможность задать этим потокам банальный affinity) абстракция.

Например - вот как может быть реализовано Poll::Pending? Видимо по всем задачамфьючам в очереди после пробуждения от подсистемы ввода вывода или при добавлении новой задачи пробегаются циклом и делают poll. Это вообще не нужная сущность в случае если ты делаешь цикл обработки событий вручную, вот ты платишь тем кодом который вызывается в poll например. Просто почувствуй разницу, когда ты просыпаешься с ppoll - ты идёшь по списку дескрипторов готовых к вводу выводу, а тут ты идёшь по списку всех дескрипторов, хоть и с минимумом телодвижений для неготовых. Скорее всего, для сокетов в том же расте более продуманная реализация под капотом, но как пример имхо должно быть понятно.

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

Zero-cost означает, что код, генерируемый компилятором, такой же эффективный, как самый лучший код, написанный вручную.

anonymous
()
Ответ на: комментарий от pon4ik

Просто почувствуй разницу, когда ты просыпаешься с ppoll - ты идёшь по списку дескрипторов готовых к вводу выводу, а тут ты идёшь по списку всех дескрипторов, хоть и с минимумом телодвижений для неготовых.

Любой хорошо написанный рантайм вызывает poll() только после того, как ему придёт уведомление о готовности через Waker::wake(). А чтобы вызывать wake() при готовности, используется старый добрый epoll(). Использование Future по эффективности ничем не отличается от ручного использования epoll(), машинный код генерируется примерно тот же самый.

anonymous
()
Ответ на: комментарий от anonymous

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

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

Это означает, что а) это говно динамическое, б) любую async-функцию нужно запускать через рантайм-раннер.

f0() {
  ...
  read.await();
}

f1() {
 ...
  f().await;
 ...
}

f2() {
 ...
 f1().await;
 ...
}

run(f2)

Здесь, рантайм не имеет доступа к вложенным футурам и не будет его иметь НИКОГДА. И запускать он сможет только f2 при резолве операции в f0.

И то, это требует рантайм-хаков дерьма, динамических аллокаций, ансейфа, миллиарда косвернных вызовов и прочей херни.

anonymous
()
Ответ на: комментарий от anonymous

Как я и предположил ниже, за той точкой которую ты цитируешь:) В общем и целом я нисколько не сомневаюсь что типовая задача почитать, сделать что-то, записать ещё куда-то должна быть реализована качественно. Более того - в расте и правда абстракция выглядит достаточно дырявой.

Судя по-всему ты конкретно с этой реализацией разобрался достаточно хорошо, интересно вот - как они реализуют функционал, когда мы кидаем обычный вызов в эту очередь задач, если работаем в рамках однопоточного executor? Когда waker разбудит, и когда цикл o5 уснёт на epoll? Насколько я помню, в boost для этого фейковый сокет использовали, как минимум какое-то время.

pon4ik ★★★★★
()

Говно раст для говнокодеров. Тьфу на вас, сопливые мамкины недопрограммисты. Хоббит - лох и стукач. Альфа жируха.

anonymous
()
Ответ на: комментарий от pon4ik

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

Просто почувствуй разницу, когда ты просыпаешься с ppoll - ты идёшь по списку дескрипторов готовых к вводу выводу

Вот здесь не zerocost-а я не вижу.

cloun1901
()
Ответ на: комментарий от anonymous

Ну в чём-то тот второй анон прав, накладные расходы на контекст (очередь задач и сами задачи, объект waker и т.п.) всё же имеются, это всё где-то да хранится. Не сложно представить state-less цикл вообще без динамического выделения памяти, особенно для задач перегнать из одного конца в другой. Сколько это и правда стоит, ну скорее всего пару тройку подключений на миллион, и обычно этим можно пренебречь конечно.

pon4ik ★★★★★
()
Ответ на: комментарий от cloun1901

Дескриптор - это чиселко в юзерспейсе, task уже как минимум чиселко + vtbl или как там полиморфизм устроен? Я не спорю, что это всё копейки которые обычно просто не стоит брать в учёт, но говорить, что оно стоит столько же сколько сколько ручной цикл обработки ввода вывода не могу представить.

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

как они реализуют функционал, когда мы кидаем обычный вызов в эту очередь задач, если работаем в рамках однопоточного executor?

Зависит от рантайма. В tokio заблокируется весь поток. По дефолту используется несколько потоков по числу процессорных ядер, но всё равно нехорошо. В async-std используется хитрый хак: если poll() выполняется слишком долго, то вместо заблокированного потока запустится новый и добавится в пул потоков: https://async.rs/blog/stop-worrying-about-blocking-the-new-async-std-runtime/. В любом случае, так делать не стоит. Есть специальные API, чтобы выполнять блокирущие операции в отдельном потоке.

anonymous
()
Ответ на: комментарий от anonymous

вопрос - где будет создан контекст и что его будет аллоцировать, а так же где будет храниться ассоциация между операцией и контекстом

Они не знают про такое. Нет контекста - нет аллокаций. Перформанс во все поля.

cloun1901
()
Ответ на: комментарий от pon4ik

Zero-cost abstraction не означает «без накладных расходов». Откуда вы вообще это взяли?

anonymous
()
Ответ на: комментарий от pon4ik

Дескриптор - это чиселко в юзерспейсе

Которое есть и в синхронном коде.

task уже как минимум чиселко + vtbl или как там полиморфизм устроен?

Никакого task и vtbl в мультиплексоре ввода-вывода нет. Либо я не понял мысль.

cloun1901
()
Ответ на: комментарий от cloun1901

Кто тебе говорил про отсутствие аллокаций, чучелко? Голоса в голове?

anonymous
()
Ответ на: комментарий от cloun1901

Тут мы возвращаемся к вопросу, как устроен future::wait. Видимо либо он хранит указатель на waker, либо waker на него, либо есть некая инверсия зависимостей но сути это не меняет.

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

Тут мы возвращаемся к вопросу, как устроен future::wait

Вызов в планировщик, как всегда. Если ты про раст и его реализацию - я об этом ничего не говорил.

cloun1901
()
Ответ на: комментарий от pon4ik

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

Нет таких задача, как и нет «объекта waker». Не повторяй херню за идиотами.

Ещё раз повторю. Контекст передаётся неявно, а это дерьмо не-полиморфно. Т.е. ты не можешь из какой-то функции в глубине получить контекст.

Именно поэтому существует waker - он не имеет никакого отношения к тому, как он называется. Ты можешь считать это просто абстрактным контекстом, как абстрактный класс у тебя в С++. Хотя им он и является, что интересно. Бездарные дошколята не смогли реализовать это через свой недоязычок.

Единственная причина почему там есть какое-то wake(КОТОРОЕ НИЧЕГО НЕ ДЕЛАЕТ - ЭТО ПРОСТО ИНТЕРФЕЙС) это то, что тип захардкоден. Ты не можешь в своём контексте создать метод govnake и вызвать его. Тип затёрт.

Это просто:

struct Waker {
  void * ptr;
  virtual wake() = 0;
}

И далее это передаётся в контекст. wake() это никак не вызвается и никак не реализуется. Ты должен это сделать сам. По-сути это просто хаки для обхода дерьмовой системы типов и говночекера.

Туда же и pin. Это обход гцешной природы бездарной скриптухи. Ничего из этого не имеет никакого отношения к async.

anonymous
()
Ответ на: комментарий от pon4ik

В целом, неважно. Кто мультиплексор предоставляет - туда и вызов.

cloun1901
()

Вообще, такое ощущение, что многие не разделяют асинхронный код и мультиплексирование блокирующих вызовов. Поэтому появляются комментарии вида «не путай threads и async».

Асинхронное выполнение - это и есть threads(не важно, в какой именно инкарнации: потоки в ядре, гринтреды или сопрограммы). Действительно асинхронно можно только ждать события. Как в случае с сигналами, например. Либо псевдо-асинхронно ждать на мультиплексоре, где на самом деле ждём синхронно, но сразу группу событий.

cloun1901
()
Ответ на: удаленный комментарий

Клоунам в ту же сторону, что и анонимусу.

WatchCat ★★★★★
()
Последнее исправление: WatchCat (всего исправлений: 1)
Ответ на: комментарий от anonymous

в рантайме строить дерево зависимостей и пытается дёргать poll минимально

Я видел как устроена дергалка фьюч. Там linked list и разделено на 2 категории активные и спящие. Пока все спят дергалка спит. Как только реактор разбудил кого-то, дергалка просыпается и дергает всех неспящих по порядку. Если фьюча выполнилась, она может еще кого-то разбудить и кинуть в конец списка неспящих и тот тоже дернется. Как кончился список бодрствующих дергалка уходит опять спать. Ну или то же самое, только есть несколько дергалок впаралель и если у одной из них кончились фьючи, она топает к загруженным соседям и тырит фьючи у них. Никаких деревьев в помине нет. С зависимостями есть другая штука, называется ECS. Там ты декларируешь с какими данными работают системы и планировщик раскидывает их в потоки так, чтоб не нарушался порядок доступа к данным. Но там вся соль в том, что система долго считает, а у фьюч соль в том, что считать там особо нечего, надо только ждать IO

q0tw4 ★★★★
()
Последнее исправление: q0tw4 (всего исправлений: 1)
Ответ на: комментарий от anonymous

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

anonymous
()
Ответ на: комментарий от rafyibofye

В результате мир просто не знает(практически) адептов раста, которые могут что-то понимать. Там их единицы на каждую фичу. Все остальные ретранслируют херню.

Не в бровь, а в глаз. Я наблюдаю подобную картину почти везде.

rupert ★★★★★
()
Ответ на: комментарий от cloun1901

Она к тому же еще и ускользающе не поясняет, куда и как таск будет вешаться на события при ио.

Tasks provide an equivalent to this model: the task “blocks” by yielding back to its executor, after installing itself as a callback for the events it’s waiting on. Returning to the example of reading from a socket, on a NotReady result the >>task can be added to the event loop’s dispatch table<<, so that it will be woken up when the socket becomes ready, at which point it will re-poll its future. Crucially, though, the task instance stays fixed for the lifetime of the future it is executing—so no allocation is needed to create or install this callback.

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

anonymous
()
Ответ на: комментарий от derlafff

Потратил целый вечер на написание очень простого приложения. Родилось что-то такое:

    async fn client_read_loop(
        r: &mut tokio_serde::Framed<FramedRead<tokio::net::tcp::OwnedReadHalf, LengthDelimitedCodec>, Message, Message, tokio_serde::formats::Json<Message, Message>>,
        w: &mut tokio_serde::Framed<FramedWrite<tokio::net::tcp::OwnedWriteHalf, LengthDelimitedCodec>, Message, Message, Json<Message, Message>>,
        ) -> Result<(), Box<dyn Error>> {

@ Начал ценить go с удвоенной силой

derlafff ★★★★★
()
Последнее исправление: derlafff (всего исправлений: 2)
Ответ на: комментарий от anonymous

Тебя даже в телеге забанили что ли?

anonymous
()
Ответ на: комментарий от derlafff

Это очень сильно напоминает C++ в 2000-х, когда все соревновались в тотальной шаблонизации своего кода. Этакая специальная олимпиада для тех, кто больше и глубже впихнёт шаблоны куда не надо.

rupert ★★★★★
()

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

bonta ★★★★★
()
Ответ на: комментарий от derlafff

Зачем тебе это tokio-serde? Тоже считаю, что все эти «кодеки» и «протоколы» — неудачная идея. Слишком сложные и неудобные абстракции, проще юзать serde напрямую.

Полезная абстракция - это трейт Stream, который просто асинхронный аналог трейта Iterator. Когда-нибудь в будущем допилят синтаксический сахар для асинхронных итераторов со всякими async yield, async for и т. д.

anonymous
()
Ответ на: комментарий от anonymous

Ну я хочу что-то вроде json.Reader в go, только асинхронный.

Да - надо посмотреть другие библиотеки.

derlafff ★★★★★
()
Ответ на: комментарий от anonymous

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

pon4ik ★★★★★
()
Ответ на: комментарий от anonymous

А я правильно понимаю, что сделать пайп неблокирующий будет правильно вот-так на async_std?:

use async_std::fs::File;
use async_std::fs::OpenOptions;
use async_std::prelude::*;
use async_std::io;
use async_std::task;

const LEN: usize = 1024;
const ITERATIONS: usize = 5 * 1000 * 1000;

fn main() -> io::Result<()> {
    task::block_on(async {
        let mut input = File::open("/dev/zero").await?;
        let mut it = 0;
        let mut output = OpenOptions::new()
            .read(false)
            .write(true)
            .open("/dev/null")
            .await?;

        //let mut output = File::open("/dev/null").await?;
        let mut buf = vec![0u8; LEN];
        loop {
            //println!("{} it", it);
            if it == ITERATIONS {
                return Ok(());
            }
            let n = input.read(&mut buf).await?;
            output.write_all(&buf[..n]).await?;
            it += 1;

        }
    })
}

Можно это как-то оптимизировать? Так, чтобы максимально было сопоставимо циклу на системных очередях?

pon4ik ★★★★★
()
Ответ на: комментарий от pon4ik

Нихрена не понял. У вас всех фундаментальная проблема с пониманием того, что это вообще такое и зачем нужно.

То дерьмо, что ты родил - не имеет смысла к существованию. И причём тут системные очереди и цикл я так же не понимаю.

Я тебе так скажу. Это дерьмо и в принципе говнораст никаким образом в рамках этой реальности не может даже близко быть равен чему-то системному. Это бездарный мусор от инвалидов для инвалидов.

На тему асинка я уже писал выше. Это эмуляция блокирующего api. Блокирующие api может быть как системным, так и не-системным, т.е. юзерспейсным. Далее уже нужно понимание и чёткие критерии, чтобы выбрать какую-то модель. Оригинальную, либо эмуляцию.

Само же по себе блокирующие api такого вида - это мусор бездарный. Он существует по одной причине - здесь программирование похожим на «поток на задачу с блокирующими операциями». К эффективности такой подход не имеет никакого отношения.

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

Хотя нет, он отличается. Отличается тем, что это страшное нелепое дерьмо.

anonymous
()
Ответ на: комментарий от anonymous

язык не имеет доступа к потоку управления

управлять планированием надо на сетевой ссанине чтобы эфективно загрузить процессор тормозной говниной, для таких языков не нужна ОС, им нужно экзоядро/libos/DOS чтобы им никто не мешал процессор говниной нагружать.

anonymous
()
Ответ на: комментарий от anonymous

Много буков мало аргументов. Да и хотелось бы услышать адептов.

pon4ik ★★★★★
()
Ответ на: комментарий от meliafaro

А 99% времени этот поток чем будет заниматься?

Висеть на семафоре, как и большинство подобных «менеджмент»-тредов.

reprimand ★★★★★
()

Вот почему тут не происходит не бельмеса??

use std::future::Future;
use std::task::{Poll, Context};
use std::pin::Pin;
use std::thread;
use std::time::Duration;
use futures::executor::block_on;
use std::io::{self, Write};

struct Counter {
    i: i32
}

impl Copy for Counter { }

impl Clone for Counter {
    fn clone (&self) -> Counter {
        Counter { i:  self.i}
    }
}

impl Future for Counter {
    type Output = String;

    fn poll(self: Pin<&mut Self>, cx: &mut Context <'_>) -> Poll<Self::Output> {
        if self.i < 20 {
            return Poll::Pending;
        } else {
            return Poll::Ready("That's all!".to_string());
        }
    }
}

impl Counter {
    fn new() -> Counter {
        Counter { i: 0 }
    }

    async fn count(&mut self) {
        let mut j = &self.i;
        for j in (0..20).rev() {
            let stdout = io::stdout();
            let mut handle = stdout.lock();
            handle.write_all(b"hello world");
        }
    }
}

fn main() {
    async {
        let mut c = Counter::new();
        block_on(c);
        c.count();
    };
    thread::sleep(Duration::from_millis(20000));
}

meliafaro ★★★★★
() автор топика
Ответ на: комментарий от meliafaro

Ну то есть тред втихую отсиживает своё, не подавая признаков жизни, и тихо умирает, не издав ни звука.

meliafaro ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.