Closed24

Rust 勉強メモ その 3

nukopynukopy

Rust のプロジェクト管理

References

  • The Book 第 7 章:肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
    • 7.1. パッケージとクレート
    • 7.2. モジュールを定義して、スコープとプライバシーを制御する
    • 7.3. モジュールツリーの要素を示すためのパス
    • 7.4. use キーワードでパスをスコープに持ち込む
    • 7.5. モジュールを複数のファイルに分割する

https://doc.rust-jp.rs/book-ja/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html

  • The Book 第 14 章:Cargo と Crates.io についてより詳しく
    • 14.1. リリースプロファイルでビルドをカスタマイズする
    • 14.2. Crates.io にクレートを公開する
    • 14.3. Cargo のワークスペース
    • 14.4. cargo install で Crates.io からバイナリをインストールする
    • 14.5. 独自のコマンドで Cargo を拡張する

https://doc.rust-jp.rs/book-ja/ch14-00-more-about-cargo.html

特に、Rust でモノレポやりたかったら 14.3 Cargo のワークスペースを参照。

https://doc.rust-jp.rs/book-ja/ch14-03-cargo-workspaces.html

モジュールシステムの概要を掴むのに以下の 2 つの記事が分かりやすかった。

  • (Qiita, 2022/08 更新) [Rust] ファイル分割によるクレート分割、モジュール分割、パッケージ分割

https://qiita.com/kerupani129/items/bd41d8a07daa31256aab

  • Rustのmodule完全に理解した。

https://zenn.dev/newgyu/articles/3b4677b4086768

Rust のモジュールシステムの用語

  • パッケージ package
    • クレートをビルドし、テストし、共有することができる Cargo の機能
    • また、1 つ以上のクレートを含んだものをパッケージと呼ぶ
  • クレート crate(crate は "木箱" って意味らしい)
    • Rust のパッケージ内における木構造をしたモジュール群
    • ライブラリ(ライブラリクレート)か実行可能ファイル(バイナリクレート)を生成する、
  • モジュール module
    • クレート内でグループ化されたコード。コードを整理し、読みやすさと再利用性を高めるためのもの。
    • モジュールは use キーワードと共に使うことで、パスの構成、スコープ、公開するか否か(モジュールの構成要素へのアクセス制御。他言語における public / private みたいな。)を決定できる
  • パス path
    • 要素(例えば構造体や関数やモジュール)に名前をつける方法

ポイントは上記の用語がどのように結びつき相互作用し、これらを使って同スコープを制御するか。

nukopynukopy

+α:Rust におけるモノレポ

+α として Cargo で複数パッケージをモノレポで管理するやり方もあるから一応触れる。npm workspace っぽく使える。

  • The Book 第 14 章:肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する

https://doc.rust-jp.rs/book-ja/ch14-03-cargo-workspaces.html

分かりやすい例がちょうど身近にあった。AtCoder Problems はいつも自分に指針を与えてくれる。

https://github.com/kenkoooo/AtCoderProblems/tree/master/atcoder-problems-backend

nukopynukopy

Rust のプロジェクトの構成要素

以下のような構成要素を持つ。

  • ワークスペース > パッケージ > クレート > モジュール

最小単位はモジュール。crates.io で配布されるのはパッケージ単位。ソースコードで import して使用したり(ライブラリクレート)、バイナリとして実行して使用したり(バイナリクレート)単位はクレート。

整理すると以下のようになる。

Rust のモジュールシステムおよびワークスペースの用語と役割は以下の通り:

  • モジュール: 関数定義や型定義等や、またはモジュールを入れ子状に持つことができる (詳細略)
  • クレート: モジュールツリー。(子孫を含めて) 複数のモジュールを持つことができる。Rust のコンパイルの単位
  • クレートルート: クレート内のルートモジュール
  • パッケージ: 0 または 1 個のライブラリクレート、および複数個のバイナリクレートを持つことができる
  • ワークスペース: 複数のパッケージを持つことができる (つまり複数のライブラリクレートを持てる)

下記より引用

https://qiita.com/kerupani129/items/bd41d8a07daa31256aab

nukopynukopy

パッケージとクレートの関係

  • パッケージ package
    • cargo init で作成される Rust のプロジェクトの単位。crates.io で配布される単位はパッケージ。
    • パッケージの中身は 1 つ以上のバイナリクレート(後述)、0 or 1 つのライブラリクレートで構成される。
    • cargo init でパッケージ作成時は、デフォルトで 1 つのバイナリクレート、0 つのライブラリクレートを持つパッケージとして作成される。
  • クレート crate
    • バイナリクレート binary crate
      • その名の通り、バイナリとして単体で動作するソフトウェア
      • 1 つのパッケージに対して、1 つ以上のバイナリクレートを持つことができる。cargo buildcargo run でビルド、実行できる単位。
      • デフォルトのエントリーポイントは src/main.rssrc/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 できない。

nukopynukopy

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 はライブラリの依存関係をパッケージに追加することを目的としている。

nukopynukopy

バイナリクレートの実行 cargo run

https://doc.rust-lang.org/cargo/commands/cargo-run.html

パッケージのバイナリクレートのデフォルトのエントリーポイント src/main.rs

パッケージに含まれるバイナリクレートのデフォルトのエントリーポイントは src/main.rs である。

ソースコード上のエントリーポイントは main 関数で、これを変更することはできない。

パッケージに複数のバイナリクレートを追加する src/bin/xxx.rs

パッケージに main.rs 以外のバイナリクレートを追加する方法を以下にまとめる。

  1. src/bin/xxx.rs を作成し、main 関数を実装する
  2. 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"
    
  3. バイナリクレートを実行する
    • 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)。明示的に設定したい場合は書いた方が良い。
nukopynukopy

並行・並列処理

勉強記録

並行・並列処理に関する一般的な知識の勉強記録

https://zenn.dev/pyteyon/scraps/a6f338ed29a6c5

公式ガイド

https://doc.rust-jp.rs/book-ja/ch16-00-concurrency.html#恐れるな並行性

https://doc.rust-lang.org/book/ch16-00-concurrency.html

References

マルチスレッド・プログラミングの道具箱

https://zenn.dev/yohhoy/articles/multithreading-toolbox

RustのTokioで非同期とグリーンスレッドを理解する

https://zenn.dev/tfutada/articles/5e87d6e7131e8e

Rustのスレッドとチャネルと共有メモリの話

https://zenn.dev/tfutada/articles/16766e3b4560db

Rust Atomics and Locksの紹介とAtomicを自分なりに整理する

https://zenn.dev/belle/articles/bcddf554a43053

https://twitter.com/kawasin73/status/1634807613518139394?s=20

他の言語との比較があってわかりやすかった(Python の GIL の話もあった)

■ テーマ 1 : 並列処理・非同期処理のアプローチ
1.1. ちょっとしたデータ分析の並列化・Python (daisuke.nishikawa)
1.2. Rustにおける並列処理 (fumita)
1.3. 並行処理・非同期処理のアプローチ Kotlin (kariya)
1.4. JavaScript の 非同期処理 Promise、async/await を理解する (74th)

https://techbookfest.org/product/iYNvSiPhrMUHxqB87BptAe?productVariantID=6MR3ejZgNAbN4YXZdVaekf

並行プログラミングのモデル

https://hackmd.io/@LINAp8NKSB60NVYc3zW5EQ/HkX39j_e2#並行プログラミングのモデル

nukopynukopy

chap 16-01 スレッドを使用してコードを同時に走らせる

https://doc.rust-jp.rs/book-ja/ch16-01-threads.html

多くの現代の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の使用法を探究しましょう。

nukopynukopy

プログラムの実行モデル:プロセス、スレッド、グリーンスレッド

  • プロセス
  • スレッド
  • スレッドのモデル
    • 1:1 モデル
      • 多くの OS では、新規スレッドを生成する API が提供されている。言語が OS の API を呼び出してスレッドを生成するモデルを 1:1 モデルと呼び、1 つの OS スレッドに対して 1 つの言語スレッドを意味する
    • M:N モデル
      • 多くのプログラミング言語がスレッドの独自の特別な実装を提供している。プログラミング言語が提供するスレッドは「グリーンスレッド」と知られ、このグリーンスレッドを使用する言語は、それを異なる数の OS スレッドの文脈で実行する。このため、グリーンスレッドのモデルは M:N モデルと呼ばれる。
      • M 個のグリーンスレッドに対して、N 個の OS スレッドがあり、M と N は必ずしも同じ数字ではない。

ランタイム

  • Rust における「ランタイム」 runtime
    • Rust で実装されたソフトウェアの全てのバイナリに含まれるコードのこと
    • 多くの言語では、より多くの機能と引き換えにランタイムのサイズが膨れ上がるのは受け入れられることだが、Rust にはほとんどゼロのランタイム(=ノーランタイム=小さなランタイムの意味)が必要で、パフォーマンスを維持するために C コードを呼び出せることを妥協できない。

Rust の標準ライブラリのスレッドの実行モデルは 1:1 モデル

M:N のグリーンスレッドモデルは、スレッドを管理するのにより大きな言語ランタイムが必要となる。そのため、Rust の標準ライブラリは、1:1 スレッドの実装のみを提供している。Rust はそのような低級言語のため、例えば、どのスレッドがいつ走るかのより詳細な制御や、より低コストの文脈切り替えなどの一面をオーバーヘッドと引き換えるなら、M:N スレッドの実装をしたクレートもある(例えば Tokio とか?)。

nukopynukopy

「スレッドをブロックする」

ハンドルに対して join を呼び出すと、ハンドルが表すスレッドが終了するまで現在実行中のスレッドをブロックする。**「スレッドをブロックする」**とは、そのスレッドが動いたり、終了したりすることを防ぐことである。

例えば、Rust の main スレッドにて、thread::spawn の戻り値である JoinHandle を使用して handle.join() を実行すると、メインスレッドがブロックされ、spawn されたスレッドの終了を待つことができる。

ref: 並行プログラミングのモデル

https://hackmd.io/@LINAp8NKSB60NVYc3zW5EQ/HkX39j_e2#並行プログラミングのモデル

nukopynukopy

スレッドで move クロージャを使用する

move キーワードを使用して、環境(thread を spawn するスコープ)で使用している値の所有権をクロージャに強制的に奪わせる。あるスレッドから別のスレッドに値の所有権を移すことができる。

https://doc.rust-jp.rs/book-ja/ch16-01-threads.html#スレッドでmoveクロージャを使用する

以下はコンパイルエラーになる。

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 トレイトで片付け時にコードを走らせる

https://doc.rust-jp.rs/book-ja/ch15-03-drop.html

なぜ spawn されたスレッドからメインスレッドの変数を参照するときに move が必要か?

コンパイラには spawn されたスレッドがどのくらいの期間走るのか分からないため、v への参照が常に有効であるかどうか把握できない。メインスレッドで定義した変数 v が drop されたとき spawn されたスレッドからの v の参照は不正な参照になる。Rust ではこの状況になることをコンパイラが未然に防いでくれる。

このコードを実行できてしまうなら、立ち上げたスレッドはまったく実行されることなく即座にバックグラウンドに置かれる可能性があります。 立ち上げたスレッドは内部にvへの参照を保持していますが、メインスレッドは、第15章で議論したdrop関数を使用して、 即座にvをドロップしています。そして、立ち上げたスレッドが実行を開始する時には、vはもう有効ではなく、 参照も不正になるのです。あちゃー!

https://doc.rust-jp.rs/book-ja/ch16-01-threads.html#スレッドでmoveクロージャを使用する:~:text=このコードを実行できてしまうなら、立ち上げたスレッドはまったく実行されることなく即座にバックグラウンドに置かれる可能性があります。 立ち上げたスレッドは内部に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();
}

vmove した後に 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
nukopynukopy

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 ドキュメンテーションは、 役に立つ情報を提供してくれます。

nukopynukopy

SyncSend トレイトで拡張可能な並行性 Extensible Concurrency with the Sync and Send Traits

https://doc.rust-jp.rs/book-ja/ch16-04-extensible-concurrency-sync-and-send.html

https://doc.rust-lang.org/book/ch16-04-extensible-concurrency-sync-and-send.html

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>> というエラーが出ました。 SendArc<T> に切り替えたら、コードはコンパイルできたわけです。

完全に Send の型からなる型も全て自動的に Send と印付けされます。生ポインタを除くほとんどの基本型も Send で、 生ポインタについては第19章で議論します。

Syncで複数のスレッドからのアクセスを許可する

Sync マーカートレイトは、Sync を実装した型は、複数のスレッドから参照されても安全であることを示唆します。言い換えると、&T (T への参照) が Send なら、型 TSync であり、参照が他のスレッドに安全に送信できることを意味します。Send 同様、基本型は Sync であり、Sync の型からのみ構成される型もまた Sync です。

Send ではなかったのと同じ理由で、スマートポインタの Rc<T> もまた Sync ではありません。RefCell<T> 型(これについては第15章で話しました)と関連する Cell<T> 系についても Sync ではありません。RefCell<T> が実行時に行う借用チェックの実装は、スレッド安全ではないのです。 スマートポインタの Mutex<T>Sync で、「複数のスレッド間で Mutex<T> を共有する」節で見たように、複数のスレッドでアクセスを共有するのに使用することができます。

SendSync を手動で実装するのは非安全である

SendSync トレイトから構成される型は自動的に SendSync にもなるので、 それらのトレイトを手動で実装する必要はありません。マーカートレイトとして、 実装すべきメソッドさえも何もありません。並行性に関連する不変条件を強制することに役立つだけなのです。

これらのトレイトを手動で実装するには、unsafe な Rust コードを実装することが関わってきます。unsafe な Rust コードを使用することについては第 19 章で語ります; とりあえず、重要な情報は、 SendSync ではない部品からなる新しい並行な型を構成するには、安全性保証を保持するために、注意深い思考が必要になる ということです。The Rustonomiconには、これらの保証とそれを保持する方法についての情報がより多くあります。

The Rustonomicon

https://doc.rust-lang.org/stable/nomicon/

https://doc.rust-jp.rs/rust-nomicon-ja/index.html

nukopynukopy

マーカートレイトとは?

マーカートレイトは Rust のトレイトシステムの一部で、それ自体はメソッドを持たず、型について何らかの特性を「マーク」するのに使われる。言い換えれば、これらのトレイトはある型が特定の特性を持つことを示すためのもので、実際にメソッドや関連型を提供するものではない。

マーカートレイトの例

以下にいくつかの標準ライブラリで定義されているマーカートレイトを示す:

  • std::marker::Send
    • このトレイトは、型がスレッド間で安全に転送できることを示す
  • std::marker::Sync
    • このトレイトは、型がスレッド間で安全に共有できることを示す
  • std::marker::Copy
    • コピートレイト。このトレイトは、型がビット単位でのコピーが可能であることを示す。このマーカートレイトが実装されている型は、その値を複製するときに clone メソッドを呼び出す代わりに、単純にビット単位でコピーを行うことが可能。

これらのマーカートレイトは、Rust の安全性を保証するのに重要な役割を果たす。例えば、Send トレイトと Sync トレイトの 2 つのマーカートレイトは、スレッド間でのデータの安全な共有を可能にするために使用され、スレッド安全性を保証する。これにより、Rustはデータ競合のない並行性を保証します。

また、マーカートレイトは自分で定義することも可能であり、コンパイラに特定の情報を与えることで、型安全性を保つのに役立つ独自の制約を設定することができます。

nukopynukopy

のこり

所有権、ライフタイム

  • Rust の 所有権、借用、ライフタイムについて初心者目線で説明と整理を試みる

https://blog.ojisan.io/rust-ownership-wakaran/

  • Rustのライフタイム推論入門

https://zenn.dev/skanehira/articles/2022-12-18-rust-liftime-elision

  • GitHub - rustviz/rustviz: Interactively Visualizing Ownership and Borrowing

最高かよ... Rust のライフタイムと所有権のメカニズムを可視化して見せてくれる本 / “GitHub - rustviz/rustviz: Interactively Visualizing Ownership and Borrowing”

https://github.com/rustviz/rustviz

https://twitter.com/yosuke_furukawa/status/1412236944856141825

  • Rust のライフタイムについてのよくある誤解

https://github.com/pretzelhammer/rust-blog/blob/master/posts/translations/jp/common-rust-lifetime-misconceptions.md

Rust のライフタイムに関して1本くらい動画撮ってもいいんじゃないかと思ったけど、これ読めば全部解決なんでみんな読んで欲しい

https://github.com/pretzelhammer/rust-blog/blob/master/posts/translations/jp/common-rust-lifetime-misconceptions.md

https://twitter.com/KOBA789/status/1386713798245904388

  • Rustの2種類の 'static

https://laysakura.github.io/2020/05/21/rust-static-lifetime-and-static-bounds/

エラーハンドリング

この人のブログ読み漁るとよさそう

https://text.baldanders.info/rust-lang/error-handling/

Option 型のメソッド一覧

https://doc.rust-lang.org/std/option/enum.Option.html#method.as_mut_slice

Rust のメモリ管理

コードレベルというより内部の仕組み。Rust のメモリ安全性について学ぶ。

Rust プログラムのメモリ管理の基礎

Rustのメモリに関する話.スタック,ヒープならびにメモリリークについて簡単に解説をして,そしてGCの話.そこから,Rustのメモリ安全性のロジックについて説明していて,非常に分かりやすかった.なるほどこう説明すればいいのか感.

Rust memory safety revolution

https://anixe.pl/blog/rust-memory-safety-revolution/

https://twitter.com/helloyuki_/status/984795524950777856?s=20&t=dqTZanW5CPkoEHWQuVtW4w

Discord が Go の GC が理由で Rust に書き直した話

https://twitter.com/blackenedgold/status/1224927316305776640?s=20&t=dqTZanW5CPkoEHWQuVtW4w

部分的にRustを導入しているDiscordがGoで書かれたキャッシュをRustで書き直したら速くなった話

  • GCによるハネがない
  • あらゆるメトリクスが向上
  • メモリ使用量が減ったのでキャッシュサイズを増やせた

記事:Why Discord is switching from Go to Rust - Discord Blog

https://discord.com/blog/why-discord-is-switching-from-go-to-rust

日本語訳:なぜDiscordはGoからRustへ移行するのか

https://misonln41.hateblo.jp/entry/2020/02/12/232853

nukopynukopy

プログラムの実行モデルについての ChatGPT との対話

自分:テキスト表示
ChatGPT の回答:引用表示

プログラムの実行モデルとは一般的にどういう意味ですか?

プログラムの実行モデルとは、プログラムが実行される際の動作の仕方や、プログラムがどのように処理されるかを示す仕組みのことを指します。具体的には、プログラムがコンピュータのメモリ上でどのように実行されるか、プログラムが呼び出す関数やライブラリの動作方法、プログラムが受け取る入力と出力の形式などが含まれます。プログラムの実行モデルは、コンピュータのアーキテクチャやプログラミング言語によって異なります。

Rust で cargo run したときのプログラムがどう動いてどう終わるかについて、プロセス、スレッドという用語を用いて説明してください。

Rustでcargo runコマンドを実行すると、Rustプログラムが実行されます。以下は、プログラムが実行される一般的な方法です。

  • プログラムが開始されると、コンピュータは新しいプロセスを作成します。プロセスとは、実行中のプログラムのインスタンスで、独自のアドレス空間を持ち、独自のメモリやCPU時間を使って動作します。
  • プロセスが開始されると、シングルスレッドで実行されます。スレッドとは、プロセスの中で実行される単一の実行フローです。スレッドを使うことで、プログラムの並列化が可能になります。
  • Rustプログラムには、mainスレッドというデフォルトのスレッドが存在します。 このスレッドで、プログラムのエントリーポイントであるmain関数が実行されます。
  • main関数は、プログラムに必要なリソースを確保し、必要なデータを読み込みます。その後、他のスレッドを作成したり、ライブラリ関数、または自前で記述した関数を呼び出したりします。
  • 複数のスレッドを作成することもできます。ただし、並列処理に関する問題を回避するために、スレッドの作成と同時に、スレッド間の同期や通信の手段を設定することが必要です。
  • プログラムが終了すると、すべてのスレッドは終了します。そして、プロセスが自動的にシャットダウンされ、プロセスが使用したメモリやリソースが解放されます。
    以上が、Rustプログラムがcargo runで実行される一般的なプロセスとスレッドの動作についての説明です。

グリーンスレッドと OS スレッドの違いを教えてください。Rust はどちらのモデルですか?

https://doc.rust-jp.rs/book-ja/ch16-01-threads.html#spawnで新規ス�%8[…]89を生成する

グリーンスレッドとOSスレッドの違いは、スレッドの管理方法にあります。
グリーンスレッドは、アプリケーションレベルのスレッドであり、ユーザースペースのランタイムがスレッドの生成、スケジューリング、同期など、あらゆるスレッドの操作を管理します。グリーンスレッドは、OSスレッドを使用する代わりに、アプリケーションレベルで生成されるため、OSがスレッドのコンテキスト切り替えを行う必要がなく、軽量で高速にスレッドを生成できます。ただし、グリーンスレッドは、ユーザースペースで実行されるため、OSスレッドよりも安全で、スレッドのスケジューリングを柔軟に制御できます。一方で、同期や待機などの操作をするときに、CPUリソースを使いつぎがないので、競合状態が発生する恐れがあります。
OSスレッドは、カーネルレベルスレッドであり、OSが直接生成、管理、スケジュールし、スレッドの操作を管理します。OSスレッドは複数のCPUコアを使用し、より高速に並列処理を実行できます。しかし、スレッドの生成には多くのコストがかかり、また、大量のスレッドを生成すると、メモリリソースを圧迫することがあります。
Rustは、OSスレッドを使用しています。しかし、スレッド生成のオーバーヘッドを減らすために、std::threadモジュールの中でグリーンスレッドと同様のスレッドローカルストレージを提供するなど、OSスレッドの使用を最適化する機能を提供しています。また、Rustの並列処理ライブラリであるstd::syncstd::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に比べて劣る場合があるため、使用する前に注意が必要です。

nukopynukopy

Cargo tips

cargo run でホットリロード

cargo install cargo-watch
cargo watch -x run

使っていない関数がある状態で cargo run を実行したときに、コンパイラが warning が出るが、それを抑えるコマンド

RUSTFLAGS="$RUSTFLAGS -A dead_code" cargo run
nukopynukopy

Mutex(mutual exclusion)についての資料集

  • sync.Mutexの仕組みを理解する (Go Conference 2023/06/02)

https://speakerdeck.com/ffjlabo/sync-dot-mutexnoshi-zu-miwoli-jie-suru

  • tokio 公式 doc: Which kind of mutex should you use?
    • std::sync::Mutex と tokio::sync::Mutex の使い分け

https://www.slideshare.net/YukiIto4/mutex-implementation-of-mutex

  • (Slideshare, 2017/01)Mutexを実装する implementation of mutex

https://www.slideshare.net/YukiIto4/mutex-implementation-of-mutex

  • 「Mutex の仕組みをRust のライブラリで確認」 : 並行処理でのプリミティブであるMutexについて、Rustのライブラリをもとに解説を行いました。 またAcquire-ReleaseといったRustの言語レベルでのメモリオーダーも解説しています。 Mutexの入門記事としてお読みください

https://techbookfest.org/product/6073216449642496?productVariantID=5274857115746304&utm_campaign=share&utm_medium=social&utm_source=twitter

  • (Zenn, 2020/10) マルチスレッド・プログラミングの道具箱

https://zenn.dev/yohhoy/articles/multithreading-toolbox

  • (Zenn, 2022/04) GoとRust - 並行プログラミング編

https://zenn.dev/nasa/articles/compare_rust_go_concurrency

nukopynukopy

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 モジュールを通じて自動的にスコープに取り込まれる。これは、毎回これらの項目を手動でインポートしなくても、これらの機能を利用できるようにするためである。

これらの項目を自動的に利用できるようにすると、コードを短く保つことができる。例えば、OptionResult などのエラーハンドリングに必要な型は頻繁に使用されるため、これらを手動でインポートする必要があると、コードが不必要に長くなる。

しかし、プレリュードが全ての場合に適しているわけではない。例えば、同じ名前の別の項目をインポートしたい場合、プレリュードが提供する項目との名前の衝突が起こる。このような場合、プレリュードを使わずに手動で必要な項目をインポートすることがある。

また、no_std 属性を使用して標準ライブラリを無効にする場合、プレリュードも使用できない。これは、組み込みシステムや OS など、標準ライブラリを使用できない環境で Rust を使用する場合に重要。

以上が、Rustのプレリュードについての基本的な説明です。プレリュードは、Rustの標準ライブラリを効率的に利用するための重要なツールです。しかし、それが提供する便利さと引き換えに、名前の衝突のリスクや、特定の環境での使用制限などの制約があることを理解しておくことが重要です。

このスクラップは2023/08/04にクローズされました