Модуль fs
Доступно только с флагом функции fs.
Асинхронные файловые утилиты.
Этот модуль содержит служебные методы для работы с файловой системой асинхронно. Это включает чтение/запись файлов и работу с каталогами.
Важное замечание
Имейте в виду, что большинство операционных систем не предоставляют асинхронные API файловой системы. Из-за этого Tokio будет использовать обычные блокирующие файловые операции "за кулисами". Это делается с использованием пула потоков spawn_blocking для их выполнения в фоновом режиме.
Модуль tokio::fs следует использовать только для обычных файлов. Попытка использовать его с, например, именованным каналом в Linux может привести к неожиданному поведению, такому как зависания при завершении работы среды выполнения. Для специальных файлов следует использовать специальные типы, такие как tokio::net::unix::pipe или AsyncFd.
В настоящее время Tokio всегда использует spawn_blocking на всех платформах, но в будущем это может быть изменено на использование асинхронных API файловой системы, таких как io_uring.
Использование
Самый простой способ использовать этот модуль — использовать служебные функции, которые работают с целыми файлами:
tokio::fs::readtokio::fs::read_to_stringtokio::fs::write
Две функции чтения считывают весь файл и возвращают его содержимое. Функция записи принимает содержимое файла и записывает это содержимое в файл. Она перезаписывает существующий файл, если он есть.
Например, чтобы прочитать файл:
#![allow(unused)] fn main() { let contents = tokio::fs::read_to_string("my_file.txt").await?; println!("Файл содержит {} строк.", contents.lines().count()); }
Чтобы перезаписать файл:
#![allow(unused)] fn main() { let contents = "Первая строка.\nВторая строка.\nТретья строка.\n"; tokio::fs::write("my_file.txt", contents.as_bytes()).await?; }
Использование File
Основной тип для взаимодействия с файлами — File. Он может использоваться для чтения и записи данного файла. Это делается с использованием трейтов AsyncRead и AsyncWrite. Этот тип обычно используется, когда вы хотите сделать что-то более сложное, чем просто прочитать или записать все содержимое за один раз.
Примечание: Важно использовать flush при записи в файл Tokio. Это связано с тем, что вызовы write возвращаются до завершения записи, а flush будет ждать завершения записи. (Запись произойдет, даже если вы не вызываете flush; это просто произойдет позже.) Это отличается от std::fs::File и связано с тем, что File использует spawn_blocking за кулисами.
Например, чтобы подсчитать количество строк в файле без загрузки всего файла в память:
#![allow(unused)] fn main() { use tokio::fs::File; use tokio::io::AsyncReadExt; let mut file = File::open("my_file.txt").await?; let mut chunk = vec![0; 4096]; let mut number_of_lines = 0; loop { let len = file.read(&mut chunk).await?; if len == 0 { // Длина ноль означает конец файла. break; } for &b in &chunk[..len] { if b == b'\n' { number_of_lines += 1; } } } println!("Файл содержит {} строк.", number_of_lines); }
Например, чтобы записать файл построчно:
#![allow(unused)] fn main() { use tokio::fs::File; use tokio::io::AsyncWriteExt; let mut file = File::create("my_file.txt").await?; file.write_all(b"Первая строка.\n").await?; file.write_all(b"Вторая строка.\n").await?; file.write_all(b"Третья строка.\n").await?; // Не забудьте вызвать `flush` после записи! file.flush().await?; }
Настройка производительности файлового ввода-вывода
Файлы Tokio используют spawn_blocking за кулисами, и это имеет серьезные последствия для производительности. Чтобы добиться хорошей производительности при файловом вводе-выводе в Tokio, рекомендуется объединять ваши операции в как можно меньше вызовов spawn_blocking.
Один пример этого различия можно увидеть, сравнив два приведенных выше примера чтения. Первый пример использует tokio::fs::read, который считывает весь файл за один вызов spawn_blocking, а затем возвращает его. Второй пример будет читать файл частями, используя множество вызовов spawn_blocking. Это означает, что второй пример, скорее всего, будет более затратным для больших файлов. (Конечно, использование частей может быть необходимо для очень больших файлов, которые не помещаются в памяти.)
Следующие примеры покажут некоторые стратегии для этого:
Запись всего содержимого за один раз
При создании файла записывайте данные в String или Vec<u8>, а затем записывайте весь файл одним вызовом spawn_blocking с помощью tokio::fs::write.
#![allow(unused)] fn main() { let mut contents = String::new(); contents.push_str("Первая строка.\n"); contents.push_str("Вторая строка.\n"); contents.push_str("Третья строка.\n"); tokio::fs::write("my_file.txt", contents.as_bytes()).await?; }
Использование буферизации
Используйте BufReader и BufWriter для буферизации множества мелких операций чтения или записи в несколько крупных. Этот пример, скорее всего, выполнит только один вызов spawn_blocking.
#![allow(unused)] fn main() { use tokio::fs::File; use tokio::io::{AsyncWriteExt, BufWriter}; let mut file = BufWriter::new(File::create("my_file.txt").await?); file.write_all(b"Первая строка.\n").await?; file.write_all(b"Вторая строка.\n").await?; file.write_all(b"Третья строка.\n").await?; // Из-за BufWriter фактическая запись и вызов spawn_blocking // происходят при вызове flush. file.flush().await?; }
Ручное использование std::fs внутри spawn_blocking
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Write}; use tokio::task::spawn_blocking; spawn_blocking(move || { let mut file = File::create("my_file.txt")?; file.write_all(b"Первая строка.\n")?; file.write_all(b"Вторая строка.\n")?; file.write_all(b"Третья строка.\n")?; // В отличие от файла Tokio, файлу std::fs // не нужен flush. io::Result::Ok(()) }).await.unwrap()?; }
Также полезно знать о File::set_max_buf_size, который управляет максимальным количеством байт, которое файл Tokio будет читать или записывать за один вызов spawn_blocking. По умолчанию это два мегабайта, но это может измениться.
Структуры
| Имя | Описание |
|---|---|
DirBuilder | Построитель для создания каталогов различными способами. |
DirEntry | Записи, возвращаемые потоком ReadDir. |
File | Ссылка на открытый файл в файловой системе. |
OpenOptions | Опции и флаги, которые можно использовать для настройки открытия файла. |
ReadDir | Чтение записей в каталоге. |
Функции
| Имя | Описание |
|---|---|
canonicalize | Возвращает каноническую, абсолютную форму пути с нормализованными промежуточными компонентами и разрешенными символическими ссылками. |
copy | Копирует содержимое одного файла в другой. Также копирует биты разрешений исходного файла в файл назначения. Перезаписывает содержимое файла назначения. |
create_dir | Создает новый пустой каталог по указанному пути. |
create_dir_all | Рекурсивно создает каталог и все его родительские компоненты, если они отсутствуют. |
hard_link | Создает новую жесткую ссылку в файловой системе. |
metadata | Получает информацию о файле, каталоге и т.д. по указанному пути. |
read | Читает все содержимое файла в вектор байтов. |
read_dir | Возвращает поток записей в каталоге. |
read_link | Читает символическую ссылку, возвращая файл, на который она указывает. |
read_to_string | Создает future, который откроет файл для чтения и прочитает все содержимое в строку. |
remove_dir | Удаляет существующий пустой каталог. |
remove_dir_all | Удаляет каталог по этому пути, предварительно удалив все его содержимое. Используйте осторожно! |
remove_file | Удаляет файл из файловой системы. |
rename | Переименовывает файл или каталог в новое имя, заменяя исходный файл, если to уже существует. |
set_permissions | Изменяет разрешения файла или каталога. |
symlink | Unix: Создает новую символическую ссылку в файловой системе. |
symlink_dir | Windows: Создает новую символическую ссылку на каталог. |
symlink_file | Windows: Создает новую символическую ссылку на файл. |
symlink_metadata | Получает метаданные файловой системы для пути. |
try_exists | Возвращает Ok(true), если путь указывает на существующую сущность. |
write | Создает future, который откроет файл для записи и запишет все содержимое contents в него. |