Rust 勉強メモ その 3
読み終わったやつ
これまでの勉強メモ
よく読むリファレンス
The Book 日本語版
Tour of Rust 日本語版
Rust By Example 日本語版
Rust By Practice(日本語版はない)
これからよむやつ
まだ Rust の概念に対してインデックスを張れた程度。ここから深く潜る必要あり。
- 🏃🏻♂️非同期処理→スクラップRust 勉強メモ - Tokio による非同期処理、mini-redis チュートリアルでやる
- 並列処理
- 所有権、ライフタイム
- エラーハンドリング
- Rust のメモリ管理の仕組み
- Rust のモジュールシステム
Rust のプロジェクト管理
References
- The Book 第 7 章:肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
- 7.1. パッケージとクレート
- 7.2. モジュールを定義して、スコープとプライバシーを制御する
- 7.3. モジュールツリーの要素を示すためのパス
- 7.4. use キーワードでパスをスコープに持ち込む
- 7.5. モジュールを複数のファイルに分割する
- The Book 第 14 章:Cargo と Crates.io についてより詳しく
- 14.1. リリースプロファイルでビルドをカスタマイズする
- 14.2. Crates.io にクレートを公開する
- 14.3. Cargo のワークスペース
- 14.4. cargo install で Crates.io からバイナリをインストールする
- 14.5. 独自のコマンドで Cargo を拡張する
特に、Rust でモノレポやりたかったら 14.3 Cargo のワークスペースを参照。
モジュールシステムの概要を掴むのに以下の 2 つの記事が分かりやすかった。
- (Qiita, 2022/08 更新) [Rust] ファイル分割によるクレート分割、モジュール分割、パッケージ分割
- Rustのmodule完全に理解した。
Rust のモジュールシステムの用語
-
パッケージ package
- クレートをビルドし、テストし、共有することができる Cargo の機能
- また、1 つ以上のクレートを含んだものをパッケージと呼ぶ
-
クレート crate(crate は "木箱" って意味らしい)
- Rust のパッケージ内における木構造をしたモジュール群
- ライブラリ(ライブラリクレート)か実行可能ファイル(バイナリクレート)を生成する、
-
モジュール module
- クレート内でグループ化されたコード。コードを整理し、読みやすさと再利用性を高めるためのもの。
- モジュールは
use
キーワードと共に使うことで、パスの構成、スコープ、公開するか否か(モジュールの構成要素へのアクセス制御。他言語における public / private みたいな。)を決定できる
-
パス path
- 要素(例えば構造体や関数やモジュール)に名前をつける方法
ポイントは上記の用語がどのように結びつき相互作用し、これらを使って同スコープを制御するか。
+α:Rust におけるモノレポ
+α として Cargo で複数パッケージをモノレポで管理するやり方もあるから一応触れる。npm workspace っぽく使える。
- The Book 第 14 章:肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
分かりやすい例がちょうど身近にあった。AtCoder Problems はいつも自分に指針を与えてくれる。
Rust のプロジェクトの構成要素
以下のような構成要素を持つ。
- ワークスペース > パッケージ > クレート > モジュール
最小単位はモジュール。crates.io で配布されるのはパッケージ単位。ソースコードで import して使用したり(ライブラリクレート)、バイナリとして実行して使用したり(バイナリクレート)単位はクレート。
整理すると以下のようになる。
Rust のモジュールシステムおよびワークスペースの用語と役割は以下の通り:
- モジュール: 関数定義や型定義等や、またはモジュールを入れ子状に持つことができる (詳細略)
- クレート: モジュールツリー。(子孫を含めて) 複数のモジュールを持つことができる。Rust のコンパイルの単位
- クレートルート: クレート内のルートモジュール
- パッケージ: 0 または 1 個のライブラリクレート、および複数個のバイナリクレートを持つことができる
- ワークスペース: 複数のパッケージを持つことができる (つまり複数のライブラリクレートを持てる)
下記より引用
パッケージとクレートの関係
-
パッケージ package
-
cargo init
で作成される Rust のプロジェクトの単位。crates.io で配布される単位はパッケージ。 - パッケージの中身は 1 つ以上のバイナリクレート(後述)、0 or 1 つのライブラリクレートで構成される。
-
cargo init
でパッケージ作成時は、デフォルトで 1 つのバイナリクレート、0 つのライブラリクレートを持つパッケージとして作成される。
-
-
クレート crate
-
バイナリクレート binary crate
- その名の通り、バイナリとして単体で動作するソフトウェア
- 1 つのパッケージに対して、1 つ以上のバイナリクレートを持つことができる。
cargo build
、cargo run
でビルド、実行できる単位。 - デフォルトのエントリーポイントは
src/main.rs
。src/bin/xxx.rs
により、エントリーポイントを増やすことができる。つまり、1 つのパッケージ内で、複数の単体で動作するバイナリを作成できる。
-
ライブラリクレート library crate
- その名の通り、他の Rust パッケージでライブラリとして import して使用されるソフトウェア
- 1 つのパッケージに対して、0 or 1 つのライブラリクレートを持つことができる。
cargo add
でパッケージにライブラリクレートを追加することができ、ソースコード上ではuse
キーワードで使用する。 - デフォルトのエントリーポイントは
src/lib.rs
。1 パッケージに 0 or 1 つのライブラリクレートしか持つことができないので基本src/lib.rs
がエントリーポイントとして使われる。 - 同一パッケージ内のバイナリクレート(、結合テスト、Examples)からライブラリクレートを import できる(参考)
-
src/lib.rs
が存在しない状態でパッケージを Crates.io に登録(cargo publish
)した場合、そのパッケージはライブラリクレートとしてcargo add
できない。
-
バイナリクレート binary crate
Rust におけるパッケージ配布 on Crates.io
Rust では、Python における PyPI、JavaScript / TypeScript における NPM のように、Rust で実装された多くのソフトウェアが Crates.io というパブリックレジストリに公開・配布されている。
Crates.io では先述したパッケージ単位でソフトウェアが配布されており、バイナリクレートをローカル環境にインストールしたり、ライブラリクレートを自身のプロジェクト(ワークスペースまたはパッケージ)に追加することができる。
バイナリクレートのインストール
- cargo install:このコマンドは指定されたクレートをビルドし、その結果生成されたバイナリをRustのインストールディレクトリ(デフォルトでは $HOME/.cargo/bin)に追加します。したがって、このコマンドは主にバイナリクレートをインストールするために使用されます。また、このコマンドはプロジェクトの依存関係には影響しません。
ライブラリクレートの追加
- cargo add:実際には、このコマンドはcargo-editという別のクレートが提供するもので、Cargo.tomlファイルに依存関係を追加するために使用します。cargo addを使用すると、指定したライブラリクレートがプロジェクトの依存関係として追加され、それによりプロジェクト内でそのライブラリの機能を使用することが可能となります。
まとめ
まとめると、cargo install
はバイナリのインストールと実行を目的とし、cargo add
はライブラリの依存関係をパッケージに追加することを目的としている。
cargo run
バイナリクレートの実行
src/main.rs
パッケージのバイナリクレートのデフォルトのエントリーポイント パッケージに含まれるバイナリクレートのデフォルトのエントリーポイントは src/main.rs
である。
ソースコード上のエントリーポイントは main
関数で、これを変更することはできない。
src/bin/xxx.rs
パッケージに複数のバイナリクレートを追加する パッケージに main.rs
以外のバイナリクレートを追加する方法を以下にまとめる。
-
src/bin/xxx.rs
を作成し、main
関数を実装する -
Cargo.toml
に以下のような[[bin]]
セクションを追加。name
はバイナリクレート名、path
はパッケージルートからのパスを記述する(src/main.rs
も明示的に指定できる!)[[bin]] name = "my-awesome-binary" path = "src/main.rs" [[bin]] name = "my-special-binary" path = "src/bin/my_special_binary.rs"
- バイナリクレートを実行する
-
src/main.rs
を実行する場合cargo run
-
src/bin/xxx.rs
を実行する場合cargo run --bin xxx
- 注意
-
main.rs
は明示的にCargo.toml
に記述しなくても OK -
src/bin
配下の Rust ファイルsrc/bin/xxx.rs
も明示的にCargo.toml
に記述しなくても OK。src/bin
配下の Rust ファイルは、Cargo が自動でファイル名xxx
をバイナリクレート名として検出してcargo run --bin [filename]
として実行できる(実例:kenkoooo/AtCoderProblems/atcoder-problems-backend)。明示的に設定したい場合は書いた方が良い。
-
-
features
並行・並列処理
勉強記録
並行・並列処理に関する一般的な知識の勉強記録
公式ガイド
References
マルチスレッド・プログラミングの道具箱
RustのTokioで非同期とグリーンスレッドを理解する
Rustのスレッドとチャネルと共有メモリの話
Rust Atomics and Locksの紹介とAtomicを自分なりに整理する
他の言語との比較があってわかりやすかった(Python の GIL の話もあった)
■ テーマ 1 : 並列処理・非同期処理のアプローチ
1.1. ちょっとしたデータ分析の並列化・Python (daisuke.nishikawa)
1.2. Rustにおける並列処理 (fumita)
1.3. 並行処理・非同期処理のアプローチ Kotlin (kariya)
1.4. JavaScript の 非同期処理 Promise、async/await を理解する (74th)
並行プログラミングのモデル
chap 16-01 スレッドを使用してコードを同時に走らせる
多くの現代のOSでは、実行中のプログラムのコードはプロセスで走り、OSは同時に複数のプロセスを管理します。 自分のプログラム内で、独立した部分を同時に実行できます。これらの独立した部分を走らせる機能をスレッドと呼びます。
プログラム内の計算を複数のスレッドに分けると、パフォーマンスが改善します。プログラムが同時に複数の作業をするからですが、 複雑度も増します。スレッドは同時に走らせることができるので、異なるスレッドのコードが走る順番に関して、 本来的に保証はありません。これは例えば以下のような問題を招きます:
- スレッドがデータやリソースに矛盾した順番でアクセスする競合状態
- 2つのスレッドがお互いにもう一方が持っているリソースを使用し終わるのを待ち、両者が継続するのを防ぐデッドロック
- 特定の状況でのみ起き、確実な再現や修正が困難なバグ
Rustは、スレッドを使用する際の悪影響を軽減しようとしていますが、それでも、マルチスレッドの文脈でのプログラミングでは、 注意深い思考とシングルスレッドで走るプログラムとは異なるコード構造が必要です。
プログラミング言語によってスレッドはいくつかの方法で実装されています。多くのOSで、新規スレッドを生成するAPIが提供されています。 言語がOSのAPIを呼び出してスレッドを生成するこのモデルを時に1:1と呼び、1つのOSスレッドに対して1つの言語スレッドを意味します。
多くのプログラミング言語がスレッドの独自の特別な実装を提供しています。プログラミング言語が提供するスレッドは、 グリーンスレッドとして知られ、このグリーンスレッドを使用する言語は、それを異なる数のOSスレッドの文脈で実行します。 このため、グリーンスレッドのモデルはM:Nモデルと呼ばれます: M個のグリーンスレッドに対して、 N個のOSスレッドがあり、MとNは必ずしも同じ数字ではありません。
各モデルには、それだけの利点と代償があり、Rustにとって最も重要な代償は、ランタイムのサポートです。 ランタイムは、混乱しやすい用語で文脈によって意味も変わります。
この文脈でのランタイムとは、言語によって全てのバイナリに含まれるコードのことを意味します。 言語によってこのコードの大小は決まりますが、非アセンブリ言語は全てある量の実行時コードを含みます。 そのため、口語的に誰かが「ノーランタイム」と言ったら、「小さいランタイム」のことを意味することがしばしばあります。 ランタイムが小さいと機能も少ないですが、バイナリのサイズも小さくなるという利点があり、 その言語を他の言語とより多くの文脈で組み合わせることが容易になります。多くの言語では、 より多くの機能と引き換えにランタイムのサイズが膨れ上がるのは、受け入れられることですが、 Rustにはほとんどゼロのランタイムが必要でパフォーマンスを維持するためにCコードを呼び出せることを妥協できないのです。
M:Nのグリーンスレッドモデルは、スレッドを管理するのにより大きな言語ランタイムが必要です。よって、 Rustの標準ライブラリは、1:1スレッドの実装のみを提供しています。Rustはそのような低級言語なので、 例えば、むしろどのスレッドがいつ走るかのより詳細な制御や、より低コストの文脈切り替えなどの一面をオーバーヘッドと引き換えるなら、 M:Nスレッドの実装をしたクレートもあります。
今やRustにおけるスレッドを定義したので、標準ライブラリで提供されているスレッド関連のAPIの使用法を探究しましょう。
プログラムの実行モデル:プロセス、スレッド、グリーンスレッド
- プロセス
- スレッド
- スレッドのモデル
- 1:1 モデル
- 多くの OS では、新規スレッドを生成する API が提供されている。言語が OS の API を呼び出してスレッドを生成するモデルを 1:1 モデルと呼び、1 つの OS スレッドに対して 1 つの言語スレッドを意味する
- M:N モデル
- 多くのプログラミング言語がスレッドの独自の特別な実装を提供している。プログラミング言語が提供するスレッドは「グリーンスレッド」と知られ、このグリーンスレッドを使用する言語は、それを異なる数の OS スレッドの文脈で実行する。このため、グリーンスレッドのモデルは M:N モデルと呼ばれる。
- M 個のグリーンスレッドに対して、N 個の OS スレッドがあり、M と N は必ずしも同じ数字ではない。
- 1:1 モデル
ランタイム
- Rust における「ランタイム」 runtime
- Rust で実装されたソフトウェアの全てのバイナリに含まれるコードのこと
- 多くの言語では、より多くの機能と引き換えにランタイムのサイズが膨れ上がるのは受け入れられることだが、Rust にはほとんどゼロのランタイム(=ノーランタイム=小さなランタイムの意味)が必要で、パフォーマンスを維持するために C コードを呼び出せることを妥協できない。
Rust の標準ライブラリのスレッドの実行モデルは 1:1 モデル
M:N のグリーンスレッドモデルは、スレッドを管理するのにより大きな言語ランタイムが必要となる。そのため、Rust の標準ライブラリは、1:1 スレッドの実装のみを提供している。Rust はそのような低級言語のため、例えば、どのスレッドがいつ走るかのより詳細な制御や、より低コストの文脈切り替えなどの一面をオーバーヘッドと引き換えるなら、M:N スレッドの実装をしたクレートもある(例えば Tokio とか?)。
「スレッドをブロックする」
ハンドルに対して join
を呼び出すと、ハンドルが表すスレッドが終了するまで現在実行中のスレッドをブロックする。**「スレッドをブロックする」**とは、そのスレッドが動いたり、終了したりすることを防ぐことである。
例えば、Rust の main スレッドにて、thread::spawn
の戻り値である JoinHandle
を使用して handle.join()
を実行すると、メインスレッドがブロックされ、spawn されたスレッドの終了を待つことができる。
ref: 並行プログラミングのモデル
move
クロージャを使用する
スレッドで move
キーワードを使用して、環境(thread を spawn するスコープ)で使用している値の所有権をクロージャに強制的に奪わせる。あるスレッドから別のスレッドに値の所有権を移すことができる。
以下はコンパイルエラーになる。
use std::thread;
use std::time::Duration;
fn main() {
move_closure_with_thread();
}
fn move_closure_with_thread() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
}
コンパイルエラー
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/sec01.rs:73:32
|
73 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
74 | println!("Here's a vector: {:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/sec01.rs:73:18
|
73 | let handle = thread::spawn(|| {
| __________________^
74 | | println!("Here's a vector: {:?}", v);
75 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
73 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
ref: Drop トレイトで片付け時にコードを走らせる
なぜ spawn されたスレッドからメインスレッドの変数を参照するときに move が必要か?
コンパイラには spawn されたスレッドがどのくらいの期間走るのか分からないため、v
への参照が常に有効であるかどうか把握できない。メインスレッドで定義した変数 v
が drop されたとき spawn されたスレッドからの v
の参照は不正な参照になる。Rust ではこの状況になることをコンパイラが未然に防いでくれる。
このコードを実行できてしまうなら、立ち上げたスレッドはまったく実行されることなく即座にバックグラウンドに置かれる可能性があります。 立ち上げたスレッドは内部にvへの参照を保持していますが、メインスレッドは、第15章で議論したdrop関数を使用して、 即座にvをドロップしています。そして、立ち上げたスレッドが実行を開始する時には、vはもう有効ではなく、 参照も不正になるのです。あちゃー!
クロージャの前に move
キーワードを付することで、コンパイラに値を借用すべきと推論させるのではなく、 クロージャに使用している値の所有権を強制的に奪わせる。以下のコードは動作する:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
v
を move
した後に drop
を実行すると、すでに所有権が移動された v
に対しての drop
になるので、以下はエラーになる。
fn ERROR_move_closure_with_thread() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
drop(v);
handle.join().unwrap();
}
コンパイルエラー
error[E0382]: use of moved value: `v`
--> src/sec01.rs:87:10
|
81 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
82 |
83 | let handle = thread::spawn(move || {
| ------- value moved into closure here
84 | println!("Here's a vector: {:?}", v);
| - variable moved due to use in closure
...
87 | drop(v);
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `chap16_concurrency` due to previous error
mpsc::channel
によるスレッド間のデータ転送
チャンネル chap. 16-02 メッセージ受け渡しを使ってスレッド間でデータを転送する
chap. 16-03 状態共有並行性 Shared state concurrency
RefCell<T>
/ Rc<T>
と Mutex<T>
/ Arc<T>
の類似性
Mutex<T>
は、Cell
系のように内部可変性を提供する。
RefCell<T>
を使用して Rc<T>
の内容を可変化できるのと同様に、Mutex<T>
を使用して Arc<T>
の内容を可変化できる。
counter
は不変なのに、その内部にある値への可変参照を得ることができたことに気付いたでしょうか; つまり、Mutex<T>
は、Cell
系のように内部可変性を提供するわけです。 第15章でRefCell<T>
を使用してRc<T>
の内容を可変化できるようにしたのと同様に、Mutex<T>
を使用してArc<T>
の内容を可変化しているのです。気付いておくべき別の詳細は、
Mutex<T>
を使用する際にあらゆる種類のロジックエラーからは、コンパイラは保護してくれないということです。第15章でRc<T>
は、循環参照を生成してしまうリスクを伴い、そうすると、2つのRc<T>
の値がお互いを参照し合い、メモリリークを引き起こしてしまうことを思い出してください。 同様に、Mutex<T>
はデッドロックを生成するリスクを伴っています。これは、処理が2つのリソースをロックする必要があり、2つのスレッドがそれぞれにロックを1つ獲得して永久にお互いを待ちあってしまうときに起こります。デッドロックに興味があるのなら、デッドロックのある Rust プログラムを組んでみてください; それからどんな言語でもいいので、ミューテックスに対してデッドロックを緩和する方法を調べて、Rust で是非、それを実装してみてください。Mutex<T>
とMutexGuard
に関する標準ライブラリの API ドキュメンテーションは、 役に立つ情報を提供してくれます。
Sync
と Send
トレイトで拡張可能な並行性 Extensible Concurrency with the Sync
and Send
Traits
Rust は言語自体に並行性機能をほとんど持たない。並行性機能のほとんどは標準ライブラリの一部であり、言語ではない。独自の並行性を書いたり 3rd party 製のクレートを利用したりして並行性を記述することができる。
しかし、Rust には言語に以下の 2 つの並行性概念が埋め込まれている:
- マーカートレイト
std::marker::Sync
std::marker::Send
Sendでスレッド間の所有権の転送を許可する
Send
マーカートレイトは、Send
を実装した型の所有権をスレッド間で転送できることを示唆します。Rust のほとんどの型はSend
ですが、参照カウンタRc<T>
を含めて一部例外があります: この型は、Rc<T>
の値をクローンし、クローンしたものの所有権を別のスレッドに転送しようとしたら、両方のスレッドが同時に参照カウントを更新できてしまうので、Send
になり得ません。このため、Rc<T>
はスレッド安全性のためのパフォーマンスの犠牲を支払わなくても済む、シングルスレッド環境で使用するために実装されているわけです。故に、Rust の型システムとトレイト境界により、
Rc<T>
の値を不安全にスレッド間で誤って送信することが絶対ないよう保証してくれるのです。 リスト16-14でこれを試みた時には、the trait Send is not implemented for Rc<Mutex<i32>>
というエラーが出ました。Send
のArc<T>
に切り替えたら、コードはコンパイルできたわけです。完全に
Send
の型からなる型も全て自動的にSend
と印付けされます。生ポインタを除くほとんどの基本型もSend
で、 生ポインタについては第19章で議論します。
Syncで複数のスレッドからのアクセスを許可する
Sync
マーカートレイトは、Sync
を実装した型は、複数のスレッドから参照されても安全であることを示唆します。言い換えると、&T
(T
への参照) がSend
なら、型T
はSync
であり、参照が他のスレッドに安全に送信できることを意味します。Send
同様、基本型はSync
であり、Sync
の型からのみ構成される型もまたSync
です。
Send
ではなかったのと同じ理由で、スマートポインタのRc<T>
もまたSync
ではありません。RefCell<T>
型(これについては第15章で話しました)と関連するCell<T>
系についてもSync
ではありません。RefCell<T>
が実行時に行う借用チェックの実装は、スレッド安全ではないのです。 スマートポインタのMutex<T>
はSync
で、「複数のスレッド間でMutex<T>
を共有する」節で見たように、複数のスレッドでアクセスを共有するのに使用することができます。
Send
と Sync
を手動で実装するのは非安全である
Send
とSync
トレイトから構成される型は自動的にSend
とSync
にもなるので、 それらのトレイトを手動で実装する必要はありません。マーカートレイトとして、 実装すべきメソッドさえも何もありません。並行性に関連する不変条件を強制することに役立つだけなのです。これらのトレイトを手動で実装するには、
unsafe
な Rust コードを実装することが関わってきます。unsafe
な Rust コードを使用することについては第 19 章で語ります; とりあえず、重要な情報は、Send
とSync
ではない部品からなる新しい並行な型を構成するには、安全性保証を保持するために、注意深い思考が必要になる ということです。The Rustonomiconには、これらの保証とそれを保持する方法についての情報がより多くあります。
The Rustonomicon
マーカートレイトとは?
マーカートレイトは Rust のトレイトシステムの一部で、それ自体はメソッドを持たず、型について何らかの特性を「マーク」するのに使われる。言い換えれば、これらのトレイトはある型が特定の特性を持つことを示すためのもので、実際にメソッドや関連型を提供するものではない。
マーカートレイトの例
以下にいくつかの標準ライブラリで定義されているマーカートレイトを示す:
-
std::marker::Send
- このトレイトは、型がスレッド間で安全に転送できることを示す
-
std::marker::Sync
- このトレイトは、型がスレッド間で安全に共有できることを示す
-
std::marker::Copy
- コピートレイト。このトレイトは、型がビット単位でのコピーが可能であることを示す。このマーカートレイトが実装されている型は、その値を複製するときに
clone
メソッドを呼び出す代わりに、単純にビット単位でコピーを行うことが可能。
- コピートレイト。このトレイトは、型がビット単位でのコピーが可能であることを示す。このマーカートレイトが実装されている型は、その値を複製するときに
これらのマーカートレイトは、Rust の安全性を保証するのに重要な役割を果たす。例えば、Send
トレイトと Sync
トレイトの 2 つのマーカートレイトは、スレッド間でのデータの安全な共有を可能にするために使用され、スレッド安全性を保証する。これにより、Rustはデータ競合のない並行性を保証します。
また、マーカートレイトは自分で定義することも可能であり、コンパイラに特定の情報を与えることで、型安全性を保つのに役立つ独自の制約を設定することができます。
のこり
所有権、ライフタイム
- Rust の 所有権、借用、ライフタイムについて初心者目線で説明と整理を試みる
- Rustのライフタイム推論入門
- GitHub - rustviz/rustviz: Interactively Visualizing Ownership and Borrowing
最高かよ... Rust のライフタイムと所有権のメカニズムを可視化して見せてくれる本 / “GitHub - rustviz/rustviz: Interactively Visualizing Ownership and Borrowing”
- Rust のライフタイムについてのよくある誤解
Rust のライフタイムに関して1本くらい動画撮ってもいいんじゃないかと思ったけど、これ読めば全部解決なんでみんな読んで欲しい
- Rustの2種類の 'static
エラーハンドリング
この人のブログ読み漁るとよさそう
Option 型のメソッド一覧
Rust のメモリ管理
コードレベルというより内部の仕組み。Rust のメモリ安全性について学ぶ。
Rust プログラムのメモリ管理の基礎
Rustのメモリに関する話.スタック,ヒープならびにメモリリークについて簡単に解説をして,そしてGCの話.そこから,Rustのメモリ安全性のロジックについて説明していて,非常に分かりやすかった.なるほどこう説明すればいいのか感.
Rust memory safety revolution
Discord が Go の GC が理由で Rust に書き直した話
部分的にRustを導入しているDiscordがGoで書かれたキャッシュをRustで書き直したら速くなった話
- GCによるハネがない
- あらゆるメトリクスが向上
- メモリ使用量が減ったのでキャッシュサイズを増やせた
記事:Why Discord is switching from Go to Rust - Discord Blog
日本語訳:なぜDiscordはGoからRustへ移行するのか
プログラムの実行モデルについての ChatGPT との対話
自分:テキスト表示
ChatGPT の回答:引用表示
プログラムの実行モデルとは一般的にどういう意味ですか?
プログラムの実行モデルとは、プログラムが実行される際の動作の仕方や、プログラムがどのように処理されるかを示す仕組みのことを指します。具体的には、プログラムがコンピュータのメモリ上でどのように実行されるか、プログラムが呼び出す関数やライブラリの動作方法、プログラムが受け取る入力と出力の形式などが含まれます。プログラムの実行モデルは、コンピュータのアーキテクチャやプログラミング言語によって異なります。
Rust で cargo run したときのプログラムがどう動いてどう終わるかについて、プロセス、スレッドという用語を用いて説明してください。
Rustで
cargo run
コマンドを実行すると、Rustプログラムが実行されます。以下は、プログラムが実行される一般的な方法です。
- プログラムが開始されると、コンピュータは新しいプロセスを作成します。プロセスとは、実行中のプログラムのインスタンスで、独自のアドレス空間を持ち、独自のメモリやCPU時間を使って動作します。
- プロセスが開始されると、シングルスレッドで実行されます。スレッドとは、プロセスの中で実行される単一の実行フローです。スレッドを使うことで、プログラムの並列化が可能になります。
- Rustプログラムには、mainスレッドというデフォルトのスレッドが存在します。 このスレッドで、プログラムのエントリーポイントである
main
関数が実行されます。- main関数は、プログラムに必要なリソースを確保し、必要なデータを読み込みます。その後、他のスレッドを作成したり、ライブラリ関数、または自前で記述した関数を呼び出したりします。
- 複数のスレッドを作成することもできます。ただし、並列処理に関する問題を回避するために、スレッドの作成と同時に、スレッド間の同期や通信の手段を設定することが必要です。
- プログラムが終了すると、すべてのスレッドは終了します。そして、プロセスが自動的にシャットダウンされ、プロセスが使用したメモリやリソースが解放されます。
以上が、Rustプログラムがcargo run
で実行される一般的なプロセスとスレッドの動作についての説明です。
グリーンスレッドと OS スレッドの違いを教えてください。Rust はどちらのモデルですか?
グリーンスレッドとOSスレッドの違いは、スレッドの管理方法にあります。
グリーンスレッドは、アプリケーションレベルのスレッドであり、ユーザースペースのランタイムがスレッドの生成、スケジューリング、同期など、あらゆるスレッドの操作を管理します。グリーンスレッドは、OSスレッドを使用する代わりに、アプリケーションレベルで生成されるため、OSがスレッドのコンテキスト切り替えを行う必要がなく、軽量で高速にスレッドを生成できます。ただし、グリーンスレッドは、ユーザースペースで実行されるため、OSスレッドよりも安全で、スレッドのスケジューリングを柔軟に制御できます。一方で、同期や待機などの操作をするときに、CPUリソースを使いつぎがないので、競合状態が発生する恐れがあります。
OSスレッドは、カーネルレベルスレッドであり、OSが直接生成、管理、スケジュールし、スレッドの操作を管理します。OSスレッドは複数のCPUコアを使用し、より高速に並列処理を実行できます。しかし、スレッドの生成には多くのコストがかかり、また、大量のスレッドを生成すると、メモリリソースを圧迫することがあります。
Rustは、OSスレッドを使用しています。しかし、スレッド生成のオーバーヘッドを減らすために、std::thread
モジュールの中でグリーンスレッドと同様のスレッドローカルストレージを提供するなど、OSスレッドの使用を最適化する機能を提供しています。また、Rustの並列処理ライブラリであるstd::sync
やstd::arc
は、並列処理を行う場合に発生するリスクを回避するために、非常に安全なスレッド同期プリミティブを提供しています。
OS スレッドを生成するシステムコールを Rust から呼ぶことはできますか?thread::spawn を使わずに低レベルな API を使用してください。
はい、RustからOSスレッドを生成するための低レベルなAPIを使用することができます。Rustの標準ライブラリにも、このためのAPIが用意されています。
std::threadモジュールのBuilder
構造体を使用して、OSスレッドを生成することができます。以下は、具体的な例です。rust use std::thread; fn main() { let builder = thread::Builder::new(); let handler = builder.spawn(|| { println!("Hello from a new thread!"); }).unwrap(); handler.join().unwrap(); }
上記の例では、
thread::Builder
構造体を使用して、OSスレッドを生成しています。spawn
メソッドを使用して、新しいスレッドを生成します。そしてjoin
メソッドを使用して、生成したスレッドの終了を待ちます。
ただし、std::thread
モジュールは、グリーンスレッドを使用してアプリケーションレベルのスレッドを生成していますので、この方法ではOSが持つスレッドの制約を優先して優先してスレッドが生成されます。より直接的にOSスレッドを生成し、管理する場合は、libc のpthread_create
関数などを呼び出す必要があります。以下は、CのPythonサンプルコードをRustに翻訳したものです。rust use std::ffi::CString; use std::mem; use std::ptr; use libc::{pthread_t, c_void, pthread_create, pthread_join}; fn main() { unsafe { let mut thread_id: pthread_t = mem::zeroed(); let attr: *const c_void = ptr::null(); let fun: *const c_void = mem::transmute(|| { println!("Hello from a new thread!"); }); let r = pthread_create(&mut thread_id, attr, fun, ptr::null_mut()); assert_eq!(r, 0); let r = pthread_join(thread_id, ptr::null_mut()); assert_eq!(r, 0); } }
上記の例では、
libc::pthread_create
関数を使用してOSスレッドを生成しています。そしてlibc::pthread_join
関数を使用して、生成したスレッドの終了を待ちます。ただし、unsafe
ブロック内での使用が必要で、スレッドの生成、スケジューリング、同期など、あらゆるスレッドの操作を自分で行う必要があります。
Rust における「ランタイム」という用語の意味を教えてください
Rustでの「ランタイム」とは、コンパイルされたプログラムに付随するライブラリのことを指します。ランタイムは、プログラムが実行されるときに共通のコードを提供します。これには、メモリ割り当て、スレッドスケジューリング、オブジェクトの開放、例外処理、型検査などが含まれます。
Rustのランタイムには、標準ライブラリが含まれています。これには、コレクションデータ型、ファイルIO、スレッド処理、ネットワーキングなど、一般的なタスクを処理するための便利な関数やマクロが含まれています。また、RustにはCとのインタフェースを容易にするランタイム用の別のライブラリがあります。
Rustは、ゼロコスト抽象化の哲学を持つため、ランタイムにはほとんどのオーバーヘッドがなく、プログラムの実行速度に影響を与えません。ただし、ランタイムが必要な場合、特定のデバイスやプラットフォームで正常に動作するために、ランタイムに内蔵されたライブラリバージョンの互換性に注意が必要です。
尚、Rustをコンパイルするときにランタイムを必要としない場合、そのプログラムはスタティックバイナリと呼ばれ、依存関係を持たずに独自のコードを実行できます。
Rust の std::thread
で作成したスレッドの一覧を取得したい。またスレッド自体に名前をつけたい。
Rustの
std::thread
モジュールでは、スレッドの一覧を取得したり、スレッドに名前をつける機能を提供していません。ただし、OSのAPIを使用することで、これらの機能を実装することができます。
例えばLinuxシステムでは、/proc
ファイルシステムを使用して、OS上で実行されているプロセスやスレッドに関する情報を読み取ることができます。以下は、Linuxでプロセスの一覧を取得するRustのサンプルコードです。use std::fs::File; use std::io::{BufRead, BufReader}; fn main() { let f = File::open("/proc/self/task").expect("file not found"); let reader = BufReader::new(f); for line in reader.lines() { println!("{}", line.unwrap()); } }
上記のコードでは、
/proc/self/task
をファイルを開いて、スレッドの一覧を取得しています。また、各スレッドにはIDがありますので、これを使用してスレッドに名前をつけるなどの処理を行うことができます。
ただし、この方法はLinuxシステムに特化しており、他のOSでは異なる実装が必要になる場合があります。また、スレッドの管理とプロセスの管理の方法が異なることに注意する必要があります。
macOS に /proc/self/task がありません
macOSでは、Linuxとは異なり、
/proc
ファイルシステムは提供されていません。そのため、Linuxで使用できる方法を使用することができません。
macOSでは、macOSの特定のAPIを使用して、プロセスの情報を取得することができます。そのためには、macOSのCocoaフレームワークを使用することができます。以下は、NSThread
クラスを使用して、macOSで実行されているスレッドの情報を取得するRustのサンプルコードです。use cocoa::base::{id, nil}; use cocoa::foundation::{NSArray, NSAutoreleasePool, NSProcessInfo, NSString}; #[repr(C)] struct Pthread { _private: [u8; 0], } impl Pthread { #[link(name = "pthread")] extern "C" { fn pthread_self() -> *mut Pthread; } fn current_thread() -> *mut Pthread { unsafe { pthread_self() } } } fn main() { unsafe { let pool = NSAutoreleasePool::new(nil); let process_info = NSProcessInfo::processInfo(nil); let thread_array: id = msg_send![process_info, valueForKey:@"_registeredNonDaemonThreads"]; let count: usize = msg_send![thread_array, count]; for i in 0..count { let ns_thread: id = msg_send![thread_array, objectAtIndex:i]; let raw: *mut Pthread = msg_send![ns_thread, pthread]; let id_string: id = msg_send![ns_thread, description]; let thread_name: id = msg_send![ns_thread, name]; let c_str: *const i8 = msg_send![thread_name, UTF8String]; let name = match c_str.is_null() { true => "", false => std::ffi::CStr::from_ptr(c_str).to_str().unwrap(), }; let id_c_str: *const i8 = msg_send![id_string, UTF8String]; let id_string = match id_c_str.is_null() { true => "", false => std::ffi::CStr::from_ptr(id_c_str).to_str().unwrap(), }; println!("Thread name: {}, ID: {:?}", name, raw); } pool.drain(); } }
上記の例では、Cocoaフレームワークの
NSProcessInfo
クラスを使用して、_registeredNonDaemonThreads
という配列を取得しています。この配列には、アプリケーションで生成されたすべてのスレッドが含まれます。for
ループを使用して、スレッドの名前とIDを取得し、それを出力しています。
上記のコードは、cocoa
というライブラリを使用しています。cocoa
は、macOSおよびiOSのCocoaフレームワークに対するRustのバインディングを提供するクレートです。
Python の標準ライブラリのスレッドと Rust の標準ライブラリのスレッドの違いについて教えてください。GIL という言葉を使用してください。
Pythonの標準ライブラリには、スレッドのサポートが含まれていますが、PythonスレッドにはGIL(グローバルインタプリタロック)と呼ばれる特徴があります。GILは、Pythonの実行中に1つのスレッドだけがPythonのバイトコードを実行できるようにするミューテックスのようなものです。つまり、複数のスレッドが同時に実行されている場合でも、1つのスレッドだけがPythonコードを実行することができます。このため、PythonのスレッドはCPUバウンドなタスクには向いていませんが、I/Oバウンドなタスクには向いています。
一方、Rustの標準ライブラリのスレッドは、GILがないため、マルチスレッド処理に非常に適しています。Rustのスレッドは、標準ライブラリのstd::thread
モジュールを使用して作成され、OSレベルのスレッド実装に依存しています。スレッドの生成、同期、スケジューリングが手軽に実装できる一方で、メモリセーフなアプリケーションの作成が容易になるよう設計されています。つまり、Rustのスレッドは、Pythonのスレッドよりも並列処理に特化しており、アプリケーションの性能を向上させることができます。
要約すると、Pythonのスレッドは、I/Oバウンドなタスクに向いていますが、CPUバウンドなタスクには向いていません。一方、Rustのスレッドは、マルチスレッド処理に適しており、メモリセーフなアプリケーションの作成にも適しています。
別解
Pythonのスレッドは、グローバルインタープリタロック(GIL)という制限によって制御されています。GILはPythonの処理系が同時に複数のスレッドを実行することをブロックし、プログラマがマルチスレッドで処理を行う場合でも最大で1つのCPUコアのみが使用されることを保証するために使用されます。
一方、Rustのスレッドは、GIL制約がなく、複数のCPUコアを使用して並列処理を行うことができます。このため、RustはPythonよりも高速で並列処理に適しています。
ただし、PythonはGILによってマルチスレッド処理が制限される一方で、そのGILによってメモリ管理に不安定なことがまれに報告されています。Rustは、明示的にメモリを管理する必要があり、その労力は高いですがメモリ管理に対する問題は少ないです。
CPython は実質シングルスレッドだね。GIL のないランタイムはないの?
CPythonのGILは、同時に複数のスレッドがPythonコードを実行することを制限するために導入されました。これにより、Pythonスレッドがデータの整合性を維持するためにロックを取得する必要がなくなり、簡単にコードを書くことができるようになりました。しかし、GILは、Pythonのマルチスレッドプログラミングが直面する主要なボトルネックの1つになっています。
そこで、GILのないPythonインタプリタ―Jython、IronPython、PyPy―があります。これらのインタプリタは、CPythonとは異なる方法でスレッドを処理しています。たとえば、PyPyは、スタックレススレッドを使用しています。これにより、Pythonコードを実行しているスレッドが同時に1つしかない場合でも、多くのスレッドをマルチスレッドのように処理することができます。
ただし、これらのGILのないインタプリタは、CPythonとは異なり、Pythonの標準ライブラリの一部が欠落していることがあります。また、これらのインタプリタの実行速度は、CPythonに比べて劣る場合があるため、使用する前に注意が必要です。
Cargo tips
cargo run でホットリロード
cargo install cargo-watch
cargo watch -x run
cargo run
を実行したときに、コンパイラが warning が出るが、それを抑えるコマンド
使っていない関数がある状態で RUSTFLAGS="$RUSTFLAGS -A dead_code" cargo run
Mutex(mutual exclusion)についての資料集
- sync.Mutexの仕組みを理解する (Go Conference 2023/06/02)
- tokio 公式 doc: Which kind of mutex should you use?
- std::sync::Mutex と tokio::sync::Mutex の使い分け
- (Slideshare, 2017/01)Mutexを実装する implementation of mutex
- 「Mutex の仕組みをRust のライブラリで確認」 : 並行処理でのプリミティブであるMutexについて、Rustのライブラリをもとに解説を行いました。 またAcquire-ReleaseといったRustの言語レベルでのメモリオーダーも解説しています。 Mutexの入門記事としてお読みください
- (Zenn, 2020/10) マルチスレッド・プログラミングの道具箱
- (Zenn, 2022/04) GoとRust - 並行プログラミング編
Rust の初期化処理(prelude)について
Rust の std::prelude
は、Rust のプログラムで一般的に使われる機能の一部を自動的にインポートするためのモジュールである。プレリュード (prelude) は、標準ライブラリの中心的な部分で、様々な基本的なトレイトや関数を提供している。これにより、Rust のプログラムは一般的なタスクを行うための基本的なツールを利用することができる。
Rustのプレリュードは、以下のような項目を含んでいる(Rust 1.53.0時点):
std::marker::{Copy, Send, Sized, Sync, Unpin}
std::ops::{Drop, Fn, FnMut, FnOnce}
std::mem::drop
std::boxed::Box
std::borrow::ToOwned
std::clone::Clone
std::cmp::{PartialEq, PartialOrd, Eq, Ord}
std::convert::{AsRef, AsMut, Into, From}
std::default::Default
std::iter::{Iterator, Extend, IntoIterator, DoubleEndedIterator, ExactSizeIterator}
std::option::Option::{self, Some, None}
std::result::Result::{self, Ok, Err}
std::string::{String, ToString}
std::vec::Vec
これらの項目は、std::prelude::v1
モジュールを通じて自動的にスコープに取り込まれる。これは、毎回これらの項目を手動でインポートしなくても、これらの機能を利用できるようにするためである。
これらの項目を自動的に利用できるようにすると、コードを短く保つことができる。例えば、Option
や Result
などのエラーハンドリングに必要な型は頻繁に使用されるため、これらを手動でインポートする必要があると、コードが不必要に長くなる。
しかし、プレリュードが全ての場合に適しているわけではない。例えば、同じ名前の別の項目をインポートしたい場合、プレリュードが提供する項目との名前の衝突が起こる。このような場合、プレリュードを使わずに手動で必要な項目をインポートすることがある。
また、no_std
属性を使用して標準ライブラリを無効にする場合、プレリュードも使用できない。これは、組み込みシステムや OS など、標準ライブラリを使用できない環境で Rust を使用する場合に重要。
以上が、Rustのプレリュードについての基本的な説明です。プレリュードは、Rustの標準ライブラリを効率的に利用するための重要なツールです。しかし、それが提供する便利さと引き換えに、名前の衝突のリスクや、特定の環境での使用制限などの制約があることを理解しておくことが重要です。