Какое хорошее подробное объяснение простого примера Tohoio TCP эхо-сервера (на GitHub и справочнике по API)? - PullRequest
0 голосов
/ 24 апреля 2020

Tokio имеет такой же пример простого эхо-сервера TCP:

Однако на обеих страницах нет объяснения того, что на самом деле происходит. Вот пример, слегка модифицированный, чтобы основная функция не возвращала Result<(), Box<dyn std::error::Error>>:

use tokio::net::TcpListener;
use tokio::prelude::*;

#[tokio::main]
async fn main() {
    if let Ok(mut tcp_listener) = TcpListener::bind("127.0.0.1:8080").await {
        while let Ok((mut tcp_stream, _socket_addr)) = tcp_listener.accept().await {
            tokio::spawn(async move {
                let mut buf = [0; 1024];
                // In a loop, read data from the socket and write the data back.
                loop {
                    let n = match tcp_stream.read(&mut buf).await {
                        // socket closed
                        Ok(n) if n == 0 => return,
                        Ok(n) => n,
                        Err(e) => {
                            eprintln!("failed to read from socket; err = {:?}", e);
                            return;
                        }
                    };
                    // Write the data back
                    if let Err(e) = tcp_stream.write_all(&buf[0..n]).await {
                        eprintln!("failed to write to socket; err = {:?}", e);
                        return;
                    }
                }
            });
        }
    }
}

После прочтения документации Tokio (https://tokio.rs/docs/overview/), вот моя ментальная модель этого пример. Задача создается для каждого нового соединения TCP. И задача завершается всякий раз, когда возникает ошибка чтения / записи, или когда клиент завершает соединение (например, n == 0 case). Следовательно, если в определенный момент времени есть 20 подключенных клиентов, будет 20 порожденных задач. Однако под капотом это НЕ эквивалентно порождению 20 потоков для одновременной обработки подключенных клиентов. Насколько я понимаю, это в основном проблема, которую пытаются решить асинхронные среды выполнения. Пока правильно?

Далее, моя ментальная модель заключается в том, что планировщик Tokio (например, многопоточный threaded_scheduler, который по умолчанию для приложений, или однопоточный basic_scheduler, который по умолчанию для тестов ) будет планировать эти задачи одновременно для потоков 1-to-N. (Дополнительный вопрос: для threaded_scheduler фиксируется ли N в течение срока службы приложения? Если да, то равно ли оно num_cpus::get()?). Если одна задача .await выполняется для операций read или write_all, то планировщик может использовать того же потока , чтобы выполнить больше работы для одной из других 19 задач. Все еще правильно?

Наконец, мне любопытно, является ли внешний код (то есть код, который .await используется для tcp_listener.accept()) сам по себе задачей? Так что в примере с 20 подключенными клиентами на самом деле не 20 задач, а 21: одна для прослушивания новых подключений + одна для каждого подключения. Все эти 21 задачи могут быть запланированы одновременно в одном или нескольких потоках, в зависимости от планировщика. В следующем примере я обертываю внешний код в дескриптор tokio::spawn и .await. Это полностью эквивалентно приведенному выше примеру?

use tokio::net::TcpListener;
use tokio::prelude::*;

#[tokio::main]
async fn main() {
    let main_task_handle = tokio::spawn(async move {
        if let Ok(mut tcp_listener) = TcpListener::bind("127.0.0.1:8080").await {
            while let Ok((mut tcp_stream, _socket_addr)) = tcp_listener.accept().await {
                tokio::spawn(async move {
                    // ... same as above ...
                });
            }
        }
    });
    main_task_handle.await.unwrap();
}

1 Ответ

0 голосов
/ 24 апреля 2020

Этот ответ является кратким изложением ответа, полученного мной от «Алисы Рил» на «Токио Раздор». Большое спасибо!

Прежде всего, действительно, для многопоточного планировщика число потоков ОС установлено на num_cpus.

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

В-третьих, основная функция выполняется в своей собственной задаче, которая порождается макросом #[tokio::main].

Следовательно, для первого примера блока кода, если есть 20 подключенных клиентов, будет 21 задача: одна для основного макроса + одна для каждого из 20 открытых потоков TCP. Для второго примера блока кода было бы 22 задачи из-за дополнительного внешнего tokio::spawn, но это не нужно и не добавляет параллелизма.

...