「The Rust Programming Language 日本語版」を読んだ備忘録⑤
16.並行処理
スレッドを生成して、複数のコードを同時に走らせる方法
【スレッドの簡単な説明】
OSでは実行中のプログラムのコードはプロセスで走り、OSは同時に複数のプロセスを管理する。
自分のプログラム内で、独立した部分を同時に実行でき、その独立した部分を走らせる機能をスレッドと呼ぶ。
プログラム内の計算を複数のスレッドに分けると同時に複数の作業を行うためパフォーマンスが改善する。
一方で複雑度が増すという害もある。
スレッドを同時に走らせることが出来るので、異なるスレッドのコードが走る順番に関して保証はない。
そのため、以下のような問題が生じる可能性もある。
・スレッドがデータやリソースに矛盾した順番でアクセスする競合状態
・2つのスレッドがお互いにもう一方が持っているリソースを使用し終わるのを待ち、両者が継続するのを防ぐデッドロック
・特定の状況でのみ起き、確実な再現や修正が困難なバグ
言語がOSのAPIを呼び出してスレッドを生成するこのモデルを時に1:1と呼び、1つのOSスレッドに対して1つの言語スレッドを意味する。
【spawnで新規スレッドを生成】
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
// output
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap(); //この行を追加することで出力が混ざらないようになる
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
// output
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
17.オブジェクト指向
カプセル化
構造体を用いた実装例
#![allow(unused)]
fn main() {
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
},
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
}
Rustでは、継承ではなくトレイトプロジェクトを使用して多相性を可能にする
18.パターンマッチング
matchアーム
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
条件分岐if let式
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
// あなたのお気に入りの色、{}を背景色に使用します
println!("Using your favorite color, {}, as the background", color);
} else if is_tuesday {
// 火曜日は緑の日!
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
// 紫を背景色に使用します
println!("Using purple as the background color");
} else {
// オレンジを背景色に使用します
println!("Using orange as the background color");
}
} else {
// 青を背景色に使用します
println!("Using blue as the background color");
}
}
while let条件分岐ループ
#![allow(unused)]
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
}
forループ
#![allow(unused)]
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
}
関数の引数
#![allow(unused)]
fn main() {
fn foo(x: i32) {
// コードがここに来る
// code goes here
}
}
19.高度な機能
19.1 Unsafe Rust
基本的にはRustのメモリ安全保証がコンパイル時に強制されている。
しかしRustにはメモリ安全保証を強制しない第2の言語(Unsafe Rust)として用いることもできる。
unsafe Rustのメリット
- 生ポインタを参照外しすること
- unsafeな関数やメソッドを呼ぶこと
- 可変で静的な変数にアクセスしたり変更すること
- unsafeなトレイトを実装すること
ただし、unsafeコードで参照を使用しても安全性チェックは行う
unsafeコードの例
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
19.2 高度なトレイト
関連型でトレイト定義においてプレースホルダーの型を指定する
関連型は、トレイトのメソッド定義がシグニチャでプレースホルダーの型を使用できるように、トレイトと型のプレースホルダーを結び付けることで
型の位置に使用される具体的な型を指定することができる
関連型があるトレイトの一例
#![allow(unused)]
fn main() {
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
}
上記では、型Itemはプレースホルダー型でnextメソッドの定義は、型OptionSelf::Itemの値を返している。
Iteratorトレイトを実装するものは、Itemの具体的な型を指定し、nextメソッドは、その具体的な型の値を含むOptionを返していることがわかる。
ジェネリクスを使用した場合、Itetratorは以下のようになる
#![allow(unused)]
fn main() {
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
}
差異は、ジェネリクスを使用すると、各実装で型を注釈しなければならないこと
トレイトにジェネリックな引数があると、 毎回ジェネリックな型引数の具体的な型を変更してある型に対して複数回実装できる。
Counterに対してnextメソッドを使用する際に、どのIteratorの実装を使用したいか型注釈をつけなければならない
関連型なら、同じ型に対してトレイトを複数回実装できないので、型を注釈する必要がないことがここで関連型を用いるメリットである。
デフォルトのジェネリック型引数と演算子オーバーロード
ジェネリックな型引数を使用する際、ジェネリックな型に対して既定の具体的な型を指定可能。
その際、既定の型が動くのであれば、トレイトを実装する側が具体的な型を指定する必要はない
このテクニックが有用になる場面の良い例が、演算子オーバーロードである。
Rustでは、独自の演算子を作ること、任意の演算子をオーバーロードすることはできない
一方、演算子に紐づいたトレイトを実装することでstd::opsに列挙された処理と対応するトレイトをオーバーロード可能となる
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 });
}
例えば、異なる単位で値を保持する構造体、MillimetersとMetersがあり、Addの実装に変換を正しくしたいばあは以下のようになる。
#![allow(unused)]
fn main() {
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
}
明確化のためのフルパス記法: 同じ名前のメソッドを呼ぶ
Rustにおいて、別のトレイトのメソッドと同じ名前のメソッドがトレイトにあったり、両方のトレイトを1つの型に実装することは問題ない。
トレイトのメソッドと同じ名前のメソッドを直接型に実装することも可能である。
例えば、以下のようにメソッド名の前にトレイト名を指定すると、コンパイラにどのflyの実装を呼び出したいか明確化できる
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
/* output
This is your captain speaking.
Up!
*waving arms furiously*
*/
一方、トレイトの一部になる関連関数においては、フルパス記法が必要となるため以下のような記述となる。
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
// println!("A baby dog is called a {}", Dog::baby_name()); どのAnimalが必要か不明の為Errorとなる
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
スーパートレイトを使用して別のトレイト内で、あるトレイトの機能を必要とする
あるトレイトに別のトレイトの機能を使用させる必要がある場合に、依存する別のトレイトを実装しているトレイトのスーパートレイトと呼ぶ。
例
#![allow(unused)]
fn main() {
trait OutlinePrint {}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
}
上記コードの場合、以下のerrorがでるため、PointにDisplayを実装してOutlinePrintが必要とする制限を満足させる必要がある。
error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter;
try using `:?` instead if you are using a format string
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
#![allow(unused)]
fn main() {
struct Point {
x: i32,
y: i32,
}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
}
感想
まだ終わりではないですが、残りは難易度が高く今まで勉強してきた内容をもう少し理解した後に改めて勉強していきたいため一旦ここで終わりにしたいと思います。
ここまでチュートリアルを読んでいく中でRustの有用性ととっつきにくさに触れることが出来た気がします。最初に触れた言語によってRustを学ぶハードルの高さが違うかなと感じました。
(例えば、pythonのような動的型式言語から移行しようとするとそれまで考えずに済んだ部分にRustのルール等が重なって難しい。何に躓いているかがわかりにくい 等)
ただ、学ぶ中でコンピューターへの理解が進む良い言語だと感じましたので、これからも少しずつ勉強していきたいと思います。
参考資料
下記資料は共に無料のため、Rustの最初の勉強にとてもおすすめです。
Discussion