Rust のreqwest を使った非同期HTTP Client のお試し


はじめに

前回の記事で,Rust でAPI Client を作成してみたが,せっかくなのでマルチスレッドで動作するプログラムも試しに書いてみたいと考えており,今回うまく動作できて処理時間も短縮できたので,記事にしてみた次第です.

今回載せているプログラムはサンプルプログラムで,マルチスレッドプログラミングがしたくなったモチベーションは業務で1アクセスにかかる時間が数十秒のオーダのものが複数あり,非同期なマルチスレッドプログラミングで処理時間を短縮できたら良いかもしれない,というかその必要に迫られるかもしれないということがあり,いろいろ調べて試してみたという形です.

プログラム

早速プログラムの内容です.

まずはじめに,Cargo.toml です.dependencies 部分だけ抜粋しています.

Cargo.toml
[dependencies]
reqwest = { version = "0.11.9", features = ["json", "blocking"] }
text-colorizer = "1"
regex = "1"
proconio = "0.3.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
ansi_term = "0.12.1"
configparser = "3.0.0"
async-std = { version = "1.7", features = ["attributes", "tokio1", "unstable"] }

次にプログラムの本体である main.rs です.

main.rs
use std::time::Instant;
use async_std::task;
use text_colorizer::*;


fn get_request(url: &str) -> std::io::Result<reqwest::blocking::Response> {

    let client = reqwest::blocking::Client::new();
    let response = client.get(url)
                         .send().unwrap();

    println!("Access {}", response.url());
    Ok(response)
}

# 非同期のHTTP Client
async fn getawait_request(url: &str) -> std::io::Result<reqwest::Response> {

    let client = reqwest::Client::new();
    let response = client.get(url)
                         .send().await.unwrap();

    println!("Access {}", response.url());
    Ok(response)
}

pub async fn many_requests(requests: Vec<String>)
                           -> Vec<std::io::Result<reqwest::Response>> {

    let mut handles = vec![];
    for url in requests {
        handles.push(task::spawn_local(async move {
            getawait_request(&url).await
        }));
    }

    let mut results = vec![];
    for handle in handles {
        results.push(handle.await);
    }

    results
}

fn main() {

    # アクセス先のURL を列挙
    let requests = vec![
    "https://www.google.com".to_string(),
    "https://www.yahoo.co.jp".to_string(),
    "https://www.oreilly.co.jp".to_string()
    ];

    # 一つずつ順に処理し,処理が完了してから次の処理を実行
    let start1 = Instant::now();
    for request in &requests {
        let result = get_request(request);
        match result {
            Ok(response) => {
                println!("{}", response.status());
            },
            Err(err) => {
                println!("NG");
                eprintln!("Err {}", err);
            },
        }
    }
    let end1 = start1.elapsed();
    # 処理時間を表示
    println!("{}: {}.{:03} [sec]", "Process time".blue().bold(), end1.as_secs(), end1.subsec_nanos() / 1_000_000);

    let start2 = Instant::now();
    # マルチスレッドで処理を実行
    let results = async_std::task::block_on(
                                   many_requests(requests));
    
    
    for result in &results {
        match result {
            Ok(response) => {
                println!("{}", response.status());
            },
            Err(err) => {
                println!("NG");
                eprintln!("Err {}", err);
            },
        }
    }
    let end2 = start2.elapsed();
    
    # マルチスレッド処理による非同期アクセスの場合の処理時間を表示
    println!("{}: {}.{:03} [sec]", "Process time".blue().bold(), end2.as_secs(), end2.subsec_nanos() / 1_000_000);
}

実行結果

コンパイルと実行は,以下のコマンドでワンライナーで実行できます.

プログラムのコンパイルと実行
cargo run

下記が実行結果です.

最初は,for ループにて,一つの処理が終われば次の処理に移っているのがわかります.シングルスレッドの場合の処理時間は0.557 [sec]でした.

実行結果(シングルスレッド)
Access https://www.google.com/
200 OK
Access https://www.yahoo.co.jp/
200 OK
Access https://www.oreilly.co.jp/index.shtml
200 OK
Process time: 0.557 [sec]
Access https://www.yahoo.co.jp/
Access https://www.google.com/
Access https://www.oreilly.co.jp/index.shtml
200 OK
200 OK
200 OK
Process time: 0.226 [sec]

一方,マルチスレッドによる非同期処理では,シングルスレッドと異なり最初のURL にアクセスした後その結果を待たずに次のURL にアクセスしている様子がわかります.マルチスレッドの場合の処理時間は0.226 [sec]でした.大体シングルスレッドの1/3 の処理時間となっており,期待されたパフォーマンスが得られていることがわかります.

実行結果(マルチスレッド)
Access https://www.yahoo.co.jp/
Access https://www.google.com/
Access https://www.oreilly.co.jp/index.shtml
200 OK
200 OK
200 OK
Process time: 0.226 [sec]

今回はただ単にアクセスしただけなので,処理時間が減っているとはいえ,大した恩恵を得ているわけではありませんが,例えばDB にアクセスして検索結果を得るような処理を実行するとなると,大きな効率化になると思います.

実際,今回マルチスレッド処理を試してみたいと思ったのは,重たいDB の検索結果を得る処理が何本もあるケースで,そのケースで上記のようなプログラムで結果を調べてみると,シングルスレッド処理の場合は4つのリクエストで合計273.026 [sec] と4分以上かかる処理が,マルチスレッド処理では69.787 [sec] と1分ちょっとで完了できました.

マルチスレッドプログラミングは,一つの処理時間が大きいほど,もしくは処理するタスクが多いほど効果を発揮するので,もしそういった問題で処理時間を短縮したい場合は是非試してみてください.

おわりに

今回実施した内容のまとめです.

  • Rust でマルチスレッドで実行するプログラムを書いた
  • シングルスレッドの場合と処理時間を比較した
  • 期待した程度の処理時間の削減が得られた

ここまで読んでいる人はいないと思いますが,もしいたらまずは,以下のリンクから全力回避フラグちゃん! チャンネルとフラグちゃんのTwitter をフォローしてください.この記事を読むより大切なことです.
大事なことなのでもう一度,チャンネル登録Twitter のフォローをよろしくお願いいたします.

参考文献

関連リンク