てけもぐ Tech 忘備録

timerfd/epoll を使った rust の非同期処理

対象

rust 初学者

内容

rust の非同期処理って初学者にはなかなか難しいと思うのですが、どうでしょう。やり方やお作法については情報が結構ある気がするんですが、理由が分からないのでスッキリしないみたいな。

私はそうだったんで、1つずつやってみることにしました。

まず、非同期処理の概念ってやつがややこしいんですけど、それは Future を扱う時に書こうかなと。書けたら。

Future を扱うにも、OS 側に仕事を預けようと思うと OS の機能を使う必要が出てくるので、まずこちらをやってみます。並列処理とか非同期処理と言うと、最初に思い浮かんだのがタイマー使った例なので、一番簡単なのをまずやって理解を深めたいなと。

javascript/typescript の promise とかだと、ランタイムがあるので良い感じにしてくれますけど、rust にはそんなものはなく低レベルの仕組みを使います。

今回は、ファイルディスクリプタ(FD)をチェックする libc::epoll と、タイマーを FD 越しに使える libc::timerfd を使いました。github にも置いてみました。1秒毎にカウントして10秒で終了します。

/*
 * Asynchronous sample with timerfd and epoll 
*/

const TOTAL: i64 = 10;
const SEC: i64 = 1;

fn main() {
    println!("#\n# count {} sec, interval {}\n#", TOTAL, SEC);

    let fd = unsafe { libc::timerfd_create(libc::CLOCK_MONOTONIC, 0) };
    if fd == -1 {
        panic!("failed libc::timerfd_create()");
    }

    // make timer struct
    let itimerspec = libc::itimerspec {
        // interval sec
        it_interval: libc::timespec {
            tv_sec: SEC,
            tv_nsec: 0,
        },
        // sec to start
        it_value: libc::timespec {
            tv_sec: SEC,
            tv_nsec: 0,
        },
    };
    // set timer with fd
    unsafe {
        libc::timerfd_settime(fd, 0, &itimerspec, std::ptr::null_mut());
    }

    // create epoll fd
    let epoll_fd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };

    // data to pass epoll. anything is fine. put fd and sample string now
    // use stack instead of heap(Box) for now
    struct DataSt {
        fd: i32,
        s: &'static str,
    }
    let mut data = DataSt {
        fd: fd,
        s: "sec passed",
    };

    // epoll accept any data structure pointer as u64(pointer size).
    // why u64?  not platform dependent ?
    //  => C uses union(epoll_data_t) which max size=u64 including void *,
    //     rust looks omitting the union and directly use void * data as u64
    let pointer_u64 = &mut data as *mut _ as u64;

    let mut event = libc::epoll_event {
        events: libc::EPOLLIN as u32,
        u64: pointer_u64,
    };
    // submit event to epoll 
    unsafe {
        libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, fd, &mut event);
    }
    // make array for multiple epoll fds
    let mut events = [event];

    // dummy buffer for libc::read
    let mut buf = [0u8; 8];
    let mut count = 0i64;
    loop {
        // epoll_wait blocks execution here and wait fd updates
        let n = unsafe { libc::epoll_wait(epoll_fd, events.as_mut_ptr(), 1, -1) };

        // check all submitted fd
        for i in 0..n as usize {
            let ev_raw_ptr = events[i].u64 as *mut DataSt;
            unsafe {
                let ev = &mut *ev_raw_ptr;
                // this read function blocks until input comes to fd
                libc::read(
                    ev.fd,
                    buf.as_mut_ptr() as *mut libc::c_void,
                    buf.len() as libc::size_t,
                );
                count += SEC;
                println!("{} {}", count, ev.s);
            }
        }
        if count >= TOTAL {
            break;
        }
    }
}

未来の自分のためにもコメントを書いておいたので、分かるかなと。

時間になって buffer にデータが入ってくるまで libc::read が処理をブロックしますので、今回の例では epoll がなくてもカウンタは作れますが、複数の非同期処理を登録するには epoll が必要になってきます。

上の例もそうなんですけど、非同期処理は結局、"コード内の"何かと何かを並列で実行しているわけではないんですよね。コード内のものを並列処理するには、スレッドが必要になってくる。ランタイムがやってくれれば別ですけど。

もちろん、メインスレッドで処理しているものをタスクとして独立させて、それと非同期処理的に処理して並列に実行させている様に見せる事は出来ます。ループ内で終了ステータスを管理しながら、それぞれのタスクを回せばいいので。それってOS でやっているタイムシェアリングでは?って感じですけど、それを一定の形で出来るのが、Future なんだと理解してます。

あと、上のコードは、epoll_ctl に渡す event 構造体とか、なるべくスタック使ってます。 Box とかヒープ関連のメモリ管理が必要なくてシンプルなんじゃないかなぁと思います。