Rust入門した + sync::Mutex.try_lock()とRAII
前々から気になっていたRustをちょっと使ってみることにしました。 Rust は C や C++ といったシステムプログラミングに用いられる言語を置き換えることを目標の1つに掲げているプログラミング言語で、 C++ 並に言語レベルできめ細かい制御、最適化が可能な一方で、後発の言語という利点を活かして、パターンマッチなどの流行りの概念も取り入れつつ、言語自体の構造もすっきりしている、というのが入門前の印象です。
加えて Rust といえば所有権という概念が導入されていることが有名です。 所有権を導入することで並行プログラミングにおいても安全性を文法レベルで保証することができるのですが、これが学習の上で鬼門になっているという噂もよく聞きます。
インストール
2018年8月28日時点では、Rust のインストールは端末で以下のコマンドを実行することが推奨されています。
$ curl https://sh.rustup.rs -sSf | sh
このコマンドは rustup と呼ばれる Rust のツールチェーンを管理するプログラムを導入して、 Rust ツールチェーンを導入するようになっています。 自分の場合、手元の Arch Linux ではリポジトリに rustup が存在していたので、パッケージ管理システムから導入しました。
ただ、ここでいきなり少しハマって、おそらく curl を使って rustup を導入すると一緒に Rust のツールチェーンを導入してくれるのですが、 パッケージ管理システムから rustup をインストールした場合、以下のように手動でツールチェーンを導入する必要がありました。
$ rustup install stable $ rustup default stable
これで Rust のコンパイラ rustc とビルドシステム cargo が使えるようになります。
開発環境
開発環境として Visual Studio Code に Rust プラグイン(Rust (rls) )を導入して用いています。
プラグインの依存を解決するため以下のコマンドを実行します。
(公式ドキュメントでは勝手に導入してくれるような記述だったけれど、自分の環境では導入されなかった?)
$ rustup component add rust-analysis $ rustup component add rls-preview $ rustup component add rust-src
この状態でプロジェクトルートを開くとよしなにやってくれます。
チュートリアル
公式ドキュメントでは プログラミング言語 Rust が勧められています。 第1版と第2版が存在していて、それぞれ日本語への翻訳プロジェクト( 第1版、第2版 )も存在しています。
この記事を書くために調べて初めて複数版存在していることを知って、自分は第1版の1.6を読んでいました。
( rust tutorial でググると日本語の記事はそれがトップだった)
ThreadとMutex
この記事の実質本題です。 第1版1.6のチュートリアルには、かの有名な食事する哲学者問題が例題として含まれています。 解法としてリソースに順序を導入し、必ず小さいリソースから確保を行う方法が提示されています。
これを各哲学者が、フォークが取れなかった場合には一旦手放して、ランダムな時間待ってからフォークを取り直すというものに変更することを考えます。 最初は誤ってこんなコードを書いていました。
fn eat(&self, table: &Table) { loop { thread::sleep(Duration::from_millis(rand::thread_rng().gen_range(10, 100))); let left_lock = match table.forks[self.left_id].try_lock() { Ok(_) => (), Err(_) => continue, }; let right_lock = match table.forks[self.right_id].try_lock() { Ok(_) => (), Err(_) => continue, }; println!("{} is eating. ({}, {})", self.name, self.left_id, self.right_id); thread::sleep(Duration::from_millis(1000)); println!("{} is done eating.", self.name); break; } }
実行してみると分かりますが、全然排他制御できていない結果が得られます。
これはロックの解法タイミングが意図したものと異なっているために起こります。 Rust の Mutex には RAII と呼ばれる仕組みが用いられています。 RAII では変数が初期化された時にロックを取得し、変数が破棄されたタイミングでロックを解放します。
つまり、上記コードでは .try_lock()
の返り値が得られた時点でロックが取得されます(もしくはロックが取得できずに Err
が返ってくる)。
次にパターンマッチに入っていますが、ここで .try_lock()
の値を使わずに処理を終えています。
排他制御がしたいだけで実際の中身には興味がないためにこのようなコードになったのですが、結果このパターンマッチを終えた時点で .try_lock()
の返り値はどこにも束縛されていないために破棄されます。
RAII では破棄と共にロックの解法も行われるので、結果としてパターンマッチを終えた時点でロックが解放されていたのです。
この問題を解決するには、.try_lock()
の返り値を実際にロックを確保したい間は適当な変数に束縛しておく必要があります。
解法としては以下のように .try_lock()
の返り値を単純に束縛したり、
let left_lock = table.forks[self.left_id].try_lock(); if let Err(_) = left_lock { continue; } let right_lock = table.forks[self.right_id].try_lock(); if let Err(_) = right_lock { continue; }
パターンマッチで Ok
が保持する値を返すというものが考えられます。
let _left_lock = match table.forks[self.left_id].try_lock() { Ok(mtx) => mtx, Err(_) => continue, }; let _right_lock = match table.forks[self.right_id].try_lock() { Ok(mtx) => mtx, Err(_) => continue, };
RAII を知っていれば別に大した話じゃなくて、ただマヌケなコードを書いているなぁという感想で終わるのですが、あまり C++ 以外で RAII を見たことが無いのも事実で、知らない人は引っかかってつらそうだなぁと思い一応記事にしました。