如何使用 Rust Tokio 處理文件及其侷限性

Rust 的 Tokio 庫以其高效處理異步 I/O 的能力而聞名,使其成爲構建高性能應用程序的熱門選擇。但是,在某些情況下,Tokio 可能無法提供顯著的優勢,例如在處理讀取大量文件時,在這個特定的上下文中,與使用普通線程池相比,Tokio 可能不是最佳的解決方案。這種限制源於這樣一個事實,即操作系統通常缺乏異步文件 api,從而削弱了 Tokio 在文件讀取任務中的潛在優勢。

值得注意的是,Tokio 在異步上下文中表現出色,例如網絡操作。如果你需要在異步上下文中讀取文件,特別是在網絡上下文中,Tokio 是首選,因爲它與異步工作流無縫集成。然而,對於性能和便利性至關重要的同步文件讀取任務,堅持使用同步 api 可能會提供一些速度優勢和更大的便利性。

使用 Tokio 處理文件

向文件寫入數據

讓我們從一個簡單但重要的任務開始:將數據異步寫入文件。save_bytes_to_file 函數演示瞭如何使用 Tokio 完成此操作。

use std::io;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

pub async fn save_bytes_to_file(data: &[u8], input_path: &str) -> io::Result<(){
    let mut file = File::create(input_path).await?;
    file.write_all(data).await?;
    Ok(())
}

這裏,我們創建一個由 input_path 指定的文件,並將提供的數據異步寫入該文件。Tokio 的 AsyncWriteExt trait 提供了 write_all 方法,簡化了異步寫操作。

從文件中讀取數據

從文件中異步讀取數據遵循類似的模式,load_bytes_from_file 函數演示瞭如何實現這一點:

use std::io;
use tokio::fs::File;
use tokio::io::AsyncReadExt;

pub async fn load_bytes_from_file(input_path: &str) -> io::Result<Vec<u8>> {
    let mut file = File::open(input_path).await?;
    let mut contents = vec![];

    file.read_to_end(&mut contents).await?;
    Ok(contents)
}

在這個函數中,打開 input_path 指定的文件,使用 read_to_end 異步讀取其內容,並將讀取的數據作爲字節向量返回。

異步文件查找和讀取

Tokio 還支持異步文件查找和讀取操作。使用 read_portion_of_file 函數,它異步讀取文件的一部分:

use std::io;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt};

pub async fn read_portion_of_file(file_path: &str, start: u64, end: u64) -> io::Result<Vec<u8>> {
    let mut file = File::open(file_path).await?;
    let mut buffer = vec![0; (end - start) as usize];

    file.seek(io::SeekFrom::Start(start)).await?;
    file.read_exact(&mut buffer).await?;

    Ok(buffer)
}

在這裏,我們查找文件中指定的起始位置,將指定的部分讀入緩衝區,並異步返回。

處理文件塊

在某些情況下,可能需要以固定大小的塊從文件中讀取數據。read_chunks_sizes_of_file 函數演示瞭如何實現這一點:

use std::io;
use tokio::fs::File;
use tokio::io::AsyncReadExt;

pub async fn read_chunks_sizes_of_file(file_path: &str) -> io::Result<Vec<u32>> {
    let mut sizes: Vec<u32> = Vec::new();
    let mut file = File::open(file_path).await?;
    let mut buffer = [0u8; 4];

    loop {
        let bytes_read = file.read(&mut buffer).await?;
        if bytes_read == 0 {
            break;
        }
        let converted_u32_from_bytes = u32::from_ne_bytes(buffer);
        sizes.push(converted_u32_from_bytes);
        file.seek(io::SeekFrom::Current(converted_u32_from_bytes as i64)).await?;
    }

    Ok(sizes)
}

這個函數在一個循環中從文件讀取數據塊,異步處理每個數據塊。

向文件追加數據

在 Tokio 中異步地向文件追加數據是很簡單的,append_to_file 函數說明了這一點:

use std::io;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;

pub async fn append_to_file(file_path: &str, data: &[u8], create_file: bool, add_bytes_size: bool) -> io::Result<(){
    let mut file = OpenOptions::new()
        .write(true)
        .append(true)
        .create(create_file)
        .open(file_path)
        .await?;

    if add_bytes_size {
        let data_length = data.len() as u32;
        let mut tmp_buffer = [0u8; 4];
        tmp_buffer.copy_from_slice(&data_length.to_le_bytes());
        file.write_all(&tmp_buffer).await?;
    }

    file.write_all(data).await?;
    Ok(())
}

在這個函數中,我們以追加模式打開文件,並在文件末尾異步寫入所提供的數據。

文件是否存在和文件大小

最後,Tokio 簡化了檢查文件存在和異步獲取文件大小的過程。函數 file_exists 和 get_file_size 演示了這個例子:

use tokio::fs;

pub async fn file_exists(file_path: &str) -> bool {
    fs::metadata(file_path).await.is_ok()
}

pub async fn get_file_size(file_path: &str) -> u64 {
    if let Ok(metadata) = fs::metadata(file_path).await {
        metadata.len()
    } else {
        0
    }
}

在這裏使用了 Tokio 的 fs::metadata 函數異步檢索文件元數據。

Tokio 在文件讀取中的侷限性

Tokio 在讀取大量文件方面可能沒有提供顯著優勢的一個關鍵原因是操作系統的本機接口中缺少異步文件 api。雖然 Tokio 擅長管理異步任務和 I/O 操作,但由於在操作系統級別缺乏對異步文件訪問的支持,它在處理文件操作時的有效性受到限制。

線程池效率

在以讀取大量文件爲主要任務的場景中,利用普通線程池通常可以產生與使用 Tokio 相當的性能。線程池有效地跨多個線程分發任務,支持併發文件讀取,而無需依賴本地異步文件 api。這種方法可以提供類似級別的並行性和效率,而不會增加集成 Tokio 異步運行時的複雜性。

複雜度開銷

將 Tokio 集成到代碼庫中會引入額外的複雜性,特別是當主要關注文件操作時。對於主要涉及同步或批處理文件讀取而沒有廣泛異步協調的任務,採用 Tokio 可能會增加不必要的複雜性,而不會帶來相應的性能提升。在這種情況下,選擇更簡單的併發模型 (例如普通線程池) 可能更合適,也更易於管理。

資源利用率

Tokio 的異步運行時旨在有效地管理線程和 I/O 操作等資源。然而,在文件讀取構成大部分工作負載且異步協調最小的場景中,Tokio 運行時管理的開銷可能會超過它的好處。這可能導致資源利用率低於最佳,並可能影響性能,特別是與普通線程池等更直接的併發模型相比。

總結

雖然 Tokio 仍然是異步編程和處理 I/O 任務的強大工具,但在同步讀取大量文件時,它的優勢可能無法完全實現。在異步文件 api 不可用且主要任務圍繞同步文件 I/O 的情況下,利用普通線程池或其他併發模型可以在複雜性較低的情況下提供相當的性能。仔細評估特定的需求和所涉及的權衡,以確定有效處理文件的最合適解決方案,這一點至關重要。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/O0f7vnzNCvvyvERifaF5KA