LoginSignup
7
4

More than 3 years have passed since last update.

[Rust] async-stdで非同期I/Oを試す

Last updated at Posted at 2020-04-25

何度 async/await の解説を読んでもいまいちわかった感じがしないので, ともかく使ってみようと試行錯誤した成果です. エコシステムの進化が速すぎて情報集めに苦労しました. 個人的に一番 async/await の必要性が高かったファイル I/O を題材として取り上げます.

問題設定

コマンドライン引数としてあるディレクトリのパスが与えられるので, そのディレクトリ直下のファイルすべてについてチェックサムを計算せよ, という課題が与えらえたとしましょう. もちろん同期版のコードならすぐに書けます (ちょっと冗長ですが非同期版と比較しやすいようにしてあります).

use std::env;

fn main() {
    use std::{fs, io::Read};

    let directory = env::args().nth(1).unwrap();
    let entries = fs::read_dir(&directory).unwrap();

    for entry in entries {
        let entry = entry.unwrap();
        if entry.file_type().unwrap().is_file() {
            let filepath = entry.path();
            let mut file = fs::File::open(&filepath).unwrap();
            let bytes = {
                let mut bytes = Vec::new();
                file.read_to_end(&mut bytes).unwrap();
                bytes
            };
            let checksum = bytes.iter()
                .fold(0u8, |acc, b| acc.wrapping_add(*b));
            println!("{:?}: {}", filepath.file_name().unwrap(), checksum);
        }
    }
}

ここで, 計算対象のファイルサイズがバラバラで, 小さなものからかなり大きなものまで存在するとしたらどうでしょう? このコードでは, 大きなファイルの I/O 待ちが発生している間, CPU はやることがありません. なんだかもったいないですね. 読み込みに時間がかかると思ったらそれはとりあえず後回しにして, 別の小さなファイルを読み込んでそのチェックサムを計算する, という風にできれば効率的な気がします. これこそが async/await が可能にしてくれる処理です.

async-std 版

std とできるだけ同じ API を提供するという触れ込みの async-std を使って書き換えてみます.

use std::env;

fn main() {
    use async_std::{fs, io::ReadExt};

    let directory = env::args().nth(1).unwrap();
    let entries = std::fs::read_dir(directory).unwrap();

    let mut handles = Vec::new();

    for entry in entries {
        let entry = entry.unwrap();
        if entry.file_type().unwrap().is_file() {
            let handle = async_std::task::spawn(async move {
                let filepath = entry.path();
                let mut file = fs::File::open(&filepath).await.unwrap();
                let bytes = {
                    let mut bytes = Vec::new();
                    file.read_to_end(&mut bytes).await.unwrap();
                    bytes
                };
                let checksum = bytes.iter()
                    .fold(0u8, |acc, b| acc.wrapping_add(*b));
                println!("{:?}: {}", filepath.file_name().unwrap(), checksum);
            });
            handles.push(handle);
        }
    }

    async_std::task::block_on(async move {
        for handle in handles.into_iter() {
            handle.await;
        }
    })
}

なんだか std::thread::spawn するのと同じような感じで書けました. time により時間を計ると, 同期版と非同期版で sys 時間はほぼ同じですが, 同期版ではおおよそ real = user + sys であるのに対して, 非同期版では明らかに real < user + sys になっています. ファイルサイズが小さい順にチェックサムが表示されますし, たぶんこれで意図通りに動いているはずです.

2020-04-26 追記 @blackenedgold さんに コメント でこのコードの問題点と改善案を教えていただきました. ありがとうございます!

参考文献

7
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4