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();
}