yikegaya’s blog

yikegayaのブログ

ReactでElement returnする時につまづいたところメモ

配列を使ったElement生成+別のコンポーネントを同時にreturnする場合

returnを「()」ではなく「[]」で囲んで配列で返す必要あり

return [
  [1, 2, 3].map((value, i) => {
    <div>{value}</div>
  }),
   <AnyComponent></AnyComponent>
]

ネストした配列からElementを生成する場合

ネストした配列の呼び出し前後にreturnを書く必要あり

return (
  [1, 2, 3].map((value, i) => {
    return (
      [1, 2, 3].map((value2, i) => {
        return (<div key={i}>{value2}</div>)
      })
    )
  })
)

AWS Cognitoのメール送信にSESを設定する時のエラー対応

AWS Cognitoの検証メール送信数はデフォルトだと上限があるのでSESを設定する必要があるけど設定する時のエラーメッセージがわかりにくてハマった。

Cognito管理画面の「E メール設定を編集」から対象のメールアドレス(Identity typeはDomain)を選択して「変更を保存」を押すと以下のエラーが出る

「Invalid FROM email address ARN」って具体的にどうすりゃいいんだ??

解決方法

SESはDNS(Route53)に登録してるレコードが多いからどっか間違ってんのかなー。と思ってRoute53回りデバッグしてたけど特に間違いらしいものが見当たらず時間を食う。

で、結局Cognitoの管理画面に戻って「返信者の名前 - オプション」に値を入れたら直った。「オプション」って表示されてるのに必須項目なの(#^ω^)ビキビキ

RailsのセッションがElastiCacheに書き込めなかった時の対応メモ

RailsのセッションをAWSのElastiCache Redisで管理しようとしていたがしばらくsession_idを保存できずハマった。Railsのバージョンは7.0.2

接続できない時の確認方法の備忘録

確認方法

まずrails consoleからRedisに繋がるか確認する。

redisのインスタンスを作る時に設定ファイルの内容と同じurlを使う

設定ファイル(config/environments/production.rb)の例

config.cache_store = :redis_cache_store, { expires_in: 1.day, url: "redis://redis-hogehuga.msqvif.ng.0001.apne1.cache.amazonaws.com:6379/0" }
bundle exec rails c
redis = Redis.new(url: "redis://redis-hogehuga.msqvif.ng.0001.apne1.cache.amazonaws.com:6379/0")
redis.ping

設定ファイルの書き方に問題があればRedis.newでエラーになるしセキュリティグループやネットワークに問題があればpingが通らない

自分の場合ここでpingが通ったから安心したらElastiCacheのエンドポイント間違えててしばらくハマった。

リーダエンドポイントではなくプライマリエンドポイントを指定しないといけないのでconsoleから適当なデータ書き込めるか一応確認した方がよかった。

redis.set("mykey", "hello world")
redis.get("mykey")

ここで書き込み不可能になっていてもrailsのcontrollerからsession作るときにエラー出してくれないっぽい

class SessionsController < ApplicationController
  def create
     user = User.find(params[:id])
     session['user_id'] = user.id
     # ここで書き込みエラーになってもrailsがraiseしてくれないっぽい
  rescue => error
     render json: { errMessage: error }
  end
end

Next.jsのapp.tsxでLayout component読み込んだ際のエラー対応メモ

Next.jsのapp.tsxのエラー対応メモ

以下のようにapp.tsxでヘッダやフッタを表示するLayoutコンポーネントを読み込む実装をしたらVSCode上でエラーが出た。

next devでは動くけど、next buildはできない状態

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Provider store={store}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </Provider>
  )
}
type Props = {
  children?: ReactNode[]
}

const Layout = ({ children, ...props }: Props): JSX.Element => {
}

エラーの内容

Type 'ReactElement<any, any>' is missing the following properties from type 'ReactNode[]': length, pop, push, concat, and 29 more.ts(2740)
Layout.tsx(29, 3): The expected type comes from property 'children' which is declared here on type 'IntrinsicAttributes & Props'

解決方法

Layout.tsxのchildrenの型を直す

type Props = {
  children?: JSX.Element[] | JSX.Element
}

写経しながら実践Rustプログラミング入門を読んだ(テスト)

前回に続いて写経しながら要点だけこの記事に書き起こす形で実践Rustプログラミング入門のPart1のCharpter3-5~を読んでみる。

www.shuwasystem.co.jp

テスト

Rustでは機能のためのコードとそれをテストするコードを同一のファイルの中で書くことができる。

pub fn add(x:i32, y:i32) -> i32 {
    return x + y;
}

#[test]
fn test_add() {
    assert_eq!(0, add(0, 0))
    assert_eq!(1, add(0, 1))
    assert_eq!(1, add(1, 0))
    assert_eq!(2, add(1, 1))
}

このテスト関数を実行するにはcargo testコマンドを実行する。

assertマクロ

テストコード中の判定に使えるassertマクロには次のようなものがある。

  • assert!()
  • assert_eq!()
  • assert_ne!()
#[test]
fn assert_sample() {
    assert!(true);

    assert!(false, "panic! value={}", false);

    assert_eq!(true, true);
    assert_ne!(true, false);

    assert_eq!(true, false, "panic! value=({} {})", true, false);
}

パニックを発生させるテスト

テストの中には、あえてパニックを発生させるテストを実施したいときがある。 そのときには、should_panicアトリビュートを使用する。

#[test]
#[should_panic]
fn test_panic() {
    panic!("expected panic");
}

普通ならpanic!マクロが実行されるのでテストは失敗するが今回のようにshould_panicアトリビュートの付いたテスト関数の場合は成功となる。逆にパニックを起こさなかった場合失敗となる。

普段は無視するテスト

#[ignore]アトリビュートを付けておくことで通常のcargo testでは実行されず無視されるようになる。

#[test]
#[ignore]
fn test_add_ignored() {
    assert_eq!(-2, add(-1, -1))
}

この無視されたテストコードは、cargo test --ignoredで実行できる。

テストモジュール

cargo new --libで新しいプロジェクトフォルダを作成すると、次のようなコードが自動で生成される。

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

#[cfg(test)]アトリビュートを指定したtestsモジュールは、cargo testを実行した時だけ、そのコードをコンパイルし、実行ファイルにそのコードを含めるようになる。

testモジュールを使用する利点は、テストコードでしか使わないヘルパー関数をこのモジュールに含めることができるので、通常のcargo buildしたバイナリファイルに不要なコードを入れないようにすることができる点。

同様に、testsモジュールの中で外部のモジュールをuseすると、テストの時だけインポートされるので、本番のバイナリファイルに不要なコードを入れずに済む。

fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(add(2, 2), 4);
    }
}

use::super*は、親ディレクトリに含まれる構造体や関数をすべて読み込むイディオム。

testsディレクト

複数のモジュールにまたがった結合テストを実行する場合には、testsディレクトリを使う。

これまではsecディレクトリの中で機能を実装してきたが、testsディレクトリはそれとはまったく別のクレートという位置付け。

testsディレクトリはsrcディレクトリで実装したクレートを外部クレートとして使用し、そのテストを実施できる。 そのため公開されている関数でのみテストで使用できる。

例えばsrc/libsに次のようなコードがあったとする。

fn add(x: i32, y: i32) -> i32 {
    return x + y;
}

これをテストするためのコードとしてtests/libs.rsに次のようなコードを書くことができる。

use test_code::add;

#[test]
fn integration_test() {
    assert_eq!(3, add(1, 2));
}

写経しながら実践Rustプログラミング入門を読んだ(クレートとモジュール、Cargo)

前回に続いて写経しながら要点だけこの記事に書き起こす形で実践Rustプログラミング入門のPart1のCharpter3-3~を読んでみる。

www.shuwasystem.co.jp

クレートとモジュール

Rustでは、ソースコードのまとまりを表す構成要素として、クレートとモジュールがある。 クレートとは、他のプログラミング言語でいう「ライブラリ」や「パッケージ」と同義の言葉である。

外部クレートの導入

外部クレートはcargo.ioというレジストリなどで見つけることができる。 cargo searchコマンドにキーワードを付与して検索すれば関連のあるクレートを検索することもできる。

外部のクレートを利用するときには、プロジェクトファイル内のCargo.tomlに利用したいクレートの情報を追記する。 Cargo.tomlの編集が終わったら、あとはcargoコマンドがビルド時に外部クレートの依存関係を解決し、必要なものをダウンロードしてくれる。

クレートの作成

自分でクレートを作成する際には、cargo new --libsにプロジェクト名を付与して実行することで、必要最低限のテンプレートを準備することができる。

このコマンドでプロジェクトディレクトリを作成するとsrcディレクトリの中にはmain.rsではなくlib.rsが作成される。

入門モジュール

1つのファイルに複数のモジュールを作る

mod module_a {
    fn calc() {
    }
}

mod module_b {
    fn calc() {
    }
}

この方法を使うと1つのファイルに複数のモジュールを書くことができる。 しかしソースコードが長くなってくればファイルが混み合ってくるのでファイルを分割する。

lib.rs

mod module_a;
mod module_b;

さらにファイルが大きくなってきた場合はサブモジュールに分割する

src/module_b.rs

mod module_c;
mod module_d;

外のソースコードからモジュールの関数や構造体を使うにはpubキーワードをつける

pub mod module_a;
mod module_b;

外部クレート内のモジュールを利用する

use new_crate::module_a;

fn main() {
}

外部クレート名をルートディレクトリと見立てて、目的までのパスを「::」でつないで指定する。

これはルートディレクトリからの絶対パス指定だが相対パス指定する際はsuperやselfを使う。

use super::super::module_a;
use self::module_c::func_c;

Cargo

Cargoはビルドシステムの役割とパッケージマネージャの役割を持つ便利ツール

Cargo.tomlの中にパッケージの情報が含まれる。使用する外部クレートはCargo.tomlの[dependencies]の下に追記していく。

cargo init

cargo newコマンドとほとんど同じ。ディレクトリ内に既にRustのソースコードがあれば今後はそれらを使ってバイナリファイルを作る。存在しなければサンプルのソースコードを生成する。

cargo build

すべての外部クレートの依存関係を解決し、必要なクレートをダウンロードした上で、ビルドを行う。

オプションで--releaseを指定すると、releaseプロファイルでビルドされる。

cargo check

パッケージと全てのクレートに対して、コードのエラーチェックを行う。

cargo run

パッケージ内のバイナリファイルを実行する。もしビルドが必要な場合はビルドを行なった後にバイナリファイルを実行する。

cargo fix

ソースコードの中にある不備や改善点を発見し、修正を自動で適用する。

cargo clean

ビルドで作られた生成物を削除する。

オプションを何も設定せずにコマンドを実行すれば、そのディレクトリごと、全ての生成物を削除する。 --releaseオプションを指定すればreleaseプロファイルの生成物のみが、-p SPECもしくは--package SPECを追加すれば指定したSPECの生成物のみが削除できる。 --docを追加すればドキュメント関連のみが削除できる。

cargo doc

cargo docコマンドはパッケージに含まれているソースコードと全ての依存関係にあるクレートについてのドキュメントを生成する。

--openオプションを追加するとドキュメント作成完了後に自動でブラウザを開いてくれる。 依存関係のあるライブラリまでドキュメントを作成したくないときには--no-depsオプションを追加する。

cargo buildすると、開発したクレートはcrates.ioに公開されるが、ドキュメントに関してはDocs.rsに公開される。

cargo install

公開されているパッケージをインストールする。

デフォルトではcrates.ioからパッケージをダウンロードするが、--gitや--pathや--registryを使うことでダウンロード元を変更できる。 インストール先のディレクトリも、デフォルトでは~/.cargoだが--root DIRオプションで変更できる。

また、環境変数CARGO_INSTALL_ROOT、Cargo.toml内のinstall.root、環境変数CARGO_HOME内にパスを指定している場合はデフォルトのパスではなく変数で指定したパスにインストールされる。

cargo uninstall

cargo uninstallコマンドはcargo installでインストールしたバイナリファイルをアンインストールする。

cargo search

creates.ioで公開されているクレートを検索できる。

cargo publish

ソースコードのパッケージを.crateファイルに圧縮し、クレートをcreates.ioにアップロードして公開する。

--dry-runオプションを追加すると、アップロードはせずに全てのチェックのみが走る。

Cargo.toml

Cargo.tomlはTOMLフォーマットで書かれたパッケージの設定ファイル。 内容はセクションごとに分かれており、パッケージ情報や依存クレート、コンパイラの設定などを記述することができる。

packageセクション

パッケージ自体の情報を記述することができる。

ターゲット関連セクション

指定したターゲットによって作れられるバイナリファイルをどのようにビルドするのかを設定できる。

セクション名 設定対象のバイナリファイル
lib ライブラリ形式のバイナリファイル
bin 実行形式のバイナリファイル
example exampleのバイナリファイル
test testのバイナリファイル
lib benchのバイナリファイル

libセクションは[lib]と大括弧で囲んでセクションを開始するが他のセクションは大括弧を二重で囲みbinと記して開始する。 このような二重で囲んだセクションはCargo.toml内で複数回書くことができ複数回書くことで、複数のバイナリファイルを作ることができる。

dependencies関連セクション

セクション名 説明
dependencies パッケージの依存関係
dev-dependencies example、tests、benchmarksのための依存関係
build-dependencies ビルドスクリプトの依存関係
target プラットフォーム特有の依存関係

Cargo.tomlとCargo.lock

cargo checkやcargo buildをするとCargo.tomlの他にCargo.lockというファイルが生成される。 Cargo.lockは外部クレートの依存関係に関する具体的な情報を含んでいる。

Cargo.lockははたくさんのクレートのバージョンが記録されているためエディタでファイルを開いて編集するようなものではない。

写経しながら実践Rustプログラミング入門を読んだ(Rustを支える言語機能)

前回に続いて写経しながら要点だけこの記事に書き起こす形で実践Rustプログラミング入門のPart1のCharpter3~を読んでみる。

www.shuwasystem.co.jp

前回の記事

写経しながら実践Rustプログラミング入門を読んだ(環境構築〜基本文法) - yikegaya’s blog

ゼロコスト抽象化

C++Javaのようなオブジェクト指向プログラミング言語にはカプセル化ポリモーフィズムのような抽象化のテクニックがある。

しかし抽象化のテクニックを使うと抽象化されたコードから具体的な動作を導くための処理を行う必要が出てくることもあり、実行時に余計な負荷がかかってしまう言語も多くある。

ゼロコスト抽象化は抽象化した処理を実行するために必要な負荷を可能なかぎり小さくすることを意味している。

traitとdyn

多くのオブジェクト指向言語ではクラスを使ってカプセル化ポリモーフィズムを実現している。一方、Rustではトレイトを使ってポリモーフィズムを実現している。

トレイトは、様々な型に共通のメソッドを実装するように促すことができる仕組み。

Javaインターフェイスに似た機能を提供し、必要なメソッドや型を定義することで、トレイトごとの共通の振る舞いをさまざまな構造体や列挙型に適応できる。

trait Tweet {
    fn tweet(&self);

    fn tweet_twice(&self) {
        self.tweet();
        self.tweet();
    }

    fn shout(&self) {
        println!("Uoooooooooooohh!!!")
    }
}

struct Dove;
struct Duck;

impl Tweet for Dove {
    fn tweet(&self) {
        println!("Coo!")
    }
}

impl Tweet for Duck {
    fn tweet(&self) {
        println!("Quack!")
    }
}

fn main() {
    let dove = Dove {};
    dove.tweet();
    dove.tweet_twice();
    dove.shout();

    let duck = Duck {};

    let bird_vec: Vec<Box<dyn Tweet>> = vec![Box::new(dove), Box::new(duck)];
    for bird in bird_vec {
        bird.tweet();
    }
}

一般的にメソッドの呼び出し方法には、動的ディスパッチと静的ディスパッチの2つがある。 動的ディスパッチはメソッドが呼び出されたとき、呼び出したインスタンスを確認し、それに合わせた処理を実行時に決める方式。 静的ディスパッチはコンパイル時に実行する処理を解決してしまう方法。

静的ディスパッチでは高速な処理ができるがどちらのインスタンスが呼び出したメソッドなのか、アプリケーションを動かしてみるまで分からない状況もある。

そうした状況ではdynを使って動的ディスパッチができる。

マーカトレイト

メソッドのない、それぞれの持つ意味や役割をしるしのように付与するトレイト。以下一例

  • Copy: 値の所有権を渡す代わりに、値のコピーを行うようにする
  • Send: スレッド境界を超えて所有権を転送できることを示す
  • Sized: メモリ上でサイズが決まっていることを示す
  • Sync: スレッド間で安全に参照を共有できることを示す

ジェネリクス

ジェネリクスを使うことでどんな型でも動作できるような処理を作れる

fn make_tuple<T, S>(t: T, s: S) -> (T, S) {
    (t, s)
}

fn main() {
    let t1 = make_tuple(1, 2);
    let t2 = make_tuple("Hello", "World");
    let t3 = make_tuple(vec!([1, 2, 3]), vec![4, 5]);
    let t4 = make_tuple(3, "years old");
}

所有権と借用

Rustの重要なキーワードとして、所有権、借用、ライフタイムという言葉がある。 Rustではそれぞれの値に所有権があり、その所有権を持っているのは必ず一つの変数だけ。

所有権は元の所有者が持ったままその値を参照する権利をもらうことを借用という。

借用している間に所有者の変数が値を破棄してしまった場合は破棄されたメモリ領域を見ることになってしまう。 そのため参照には安全に利用できる期間を明確にする必要がある。この期間をライフタイムと呼ぶ。

ムーブセマンティクス

多くのプログラミング言語では、ある変数と別の変数を=でつなげれば、値が右から左へとコピーされた。 コピーなので右の変数も同じ値が残っていて左の変数が同じ値を持つことになる。

Rustの場合変数は常に束縛され、その所有権を持つことになる。 ある変数と別の変数を=でつなげれば、右の変数が所有していた値が左の変数の所有になる。

struct Color {
    r: i32,
    g: i32,
    b: i32
}

fn main() {
    let a = Color{r:255, g:255, b:255};
    let b = a; // 所有権が譲渡される
    println!("{} {} {}", b.r, b.g, b.b)
}

借用

ある関数に引数で値を渡すとき、値の所有権ごと渡してしまうと呼び出し元の処理に再び所有権を返すのは面倒。

fn main() {
    let mut important_data = "Hello, World!".to_string();

    important_data = calc_data(important_data);

    println!("{}", important_data);
}

fn calc_data(data: String) -> String{
    println!("{}", data);
    data
}

参照を使って書くとこうなる。値の所有権を譲渡するのではなく値へのアクセスを許す方法。

fn main() {
    let important_data = "Hello, World!".to_string();

    calc_data(&important_data);

    println!("{}", &important_data)
}

fn calc_data(data: &String) {
    println!("{}", data)
}

不変な参照はいくつでも参照を渡すことができる。

fn main() {
    let x = 5;

    let y = &x;

    let z = &x;

    dbg!(x);

    dbg!(y);

    dbg!(z);
}

一方、可変な参照の場合、一度に渡せる数は1つだけ。

fn main() {
    let mut x = 5;

    {
        let y = &mut x;

        let z = &mut x; //ここでエラー

        dbg!(y);

        dbg!(z);
    }

    {
        let y = &x;

        let z = &mut x; //ここでエラー

        dbg!(y);

        dbg!(z);
    }
}

また、変更の可否に関わらず、参照は元の所有者のライフサイクルよりも長く生存できない。

fn main() {
    let y;

    {
        let x = 5;

        y = &x;

        dbg!(x);
    }

    dbg(y); // xよりもyが長く生存できずエラー
}

RAII

RAIIは「Resorce Acquisition Is Initialization」の略で「リソースの取得は初期化である」という意味。 変数の初期化時にリソースの解放を行い、変数を破棄するときにリソースの解放を行うということ。 これを正しく行わないと、リソースのリークの問題に遭遇することになる。

Rustの場合は変数がスコープから抜けたりすれば即座に確保していたリソースも解放されることになる。

リソースはメモリだけでなく開いているファイルや、ネットワーク接続なども解放される。

RustではデストラクトとしてDropトレイトというものが用意されている。

struct Droppable;

impl Drop for Droppable {
    fn drop(&mut self) {
        println!("Resource will be released!");
    }
}

fn main() {
    {
        let d = Droppable;
    }
    println!("The Droppable should be released at the end of block.");
}

スレッド安全性

スレッドを扱うには標準ライブラリのthreadを使う。thread::spawnはクロージャを受け取り、それを新しいスレッドで実行する関数。

use std::thread;

fn main() {
    thread::spawn(|| {
        println!("Hello, world");
    });
}

これを実行しても"Hello World"が表示される前にプログラムが終了して何も表示されない場合がある。 必ず表示してから終了するには次のようにする。

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        println!("Hello, world!");
    });

    dbg!(handle.join());
}

thread::spawnからスレッドのハンドルが返ってくるので、そのハンドルのjoinを呼ぶことでスレッドの終了を待つことができる。

spawnに渡すものはクロージャなので変数をスレッドに渡すことができる。

use std::thread;

fn main() {
    let mut handles = Vec::new();

    for x in 0..10 {
        handles.push(thread::spawn(|| {
            println!("Hello, world!: {}", x);
        }));
    }

    for handle in handles {
        let _ = handle.join();
    }
}

この書き方だとクロージャがデフォルトでは変数の参照をキャプチャするためエラーになる。 スレッドの生存期間は変わらないためもしかしたらxの寿命よりも長くなるかもしれない。

このエラーを修正するにはmoveキーワードを使ってxの所有権をスレッドに移す。

use std::thread;

fn main() {
    let mut handles = Vec::new();

    for x in 0..10 {
        handles.push(thread::spawn(move || {
            println!("Hello, world!: {}", x);
        }));
    }

    for handle in handles {
        let _ = handle.join();
    }
}

スレッド間の情報共有

実際のマルチスレッドプログラミングでは各スレッドは実行中も様々な情報を共有しながら協調して作業を進行させる。 マルチスレッドプログラミングにおいてスレッド間で情報を共有する方法としてRustでは次の2つの方法が提供される。

  • 共有メモリ
  • メッセージパッシング

共有メモリ

共有メモリはその名の通り複数のスレッドで同じメモリ領域を共有する方法。

use std::thread;

fn main() {
    let mut handles = Vec::new();

    let mut data = vec![1; 10];

    for x in 0..10 {
        handles.push(thread::spawn(move || {
            data[x] += 1;
        }));
    }

    for handle in handles {
        let _ = handle.join();
    }

    dbg!(data);
}

このコードはdataの所有権が1つ目のスレッドに移動したため、2つ目のスレッドで所有権を取ることができずエラーになる。 このエラーを解消するには各スレッドで所有権を共有する方法があるが、その目的のためにRcという型が用意されている。

Rcは所有権を共有するために、所有権を保持している所有者の数をカウントしている。所有者が0になったところでメモリを解放するという仕組み。

use std::rc::Rc;
use std::thread;

fn main() {
    let mut handles = Vec::new();

    let mut data = Rc::new(vec![1; 10]);

    for x in 0..10 {
        let data_ref = data.clone();
        handles.push(thread::spawn(move || {
            data_ref[x] += 1;
        }));
    }

    for handle in handles {
        let _ = handle.join();
    }

    dbg!(data);
}

これをコンパイルすると別のエラーになる。 今度は「Rcがスレッド間を安全に渡せない」というメッセージでエラー。 実はRcはマルチスレッドでは正しく動作しない。Rcが持つ参照カウンタは、複数のスレッドが同時にアクセスした場合に壊れてしまう可能性があるため。

そのためRcのマルチスレッド版としてArcというものが用意されている。(ArcはAtomically Referenced Countedの略)

以下単純にRcをArcに置き換えた実装

use std::sync::Arc;
use std::thread;

fn main() {
    let mut handles = Vec::new();

    let mut data = Arc::new(vec![1; 10]);

    for x in 0..10 {
        let data_ref = data.clone();
        handles.push(thread::spawn(move || {
            data_ref[x] += 1;
        }));
    }

    for handle in handles {
        let _ = handle.join();
    }

    dbg!(data);
}

再度コンパイルするとまた別のエラーになる。 Arcは書き換え不能。今回のケースでは各スレッドが別々の要素を参照することがわかっているが一般的には必ずしもそうとは限らない。 そのためArcでは書き換えを行うことはせず、別途排他制御を行う構造が必要になる。そのような用途のために提供されているのがMutex

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let mut handles = Vec::new();

    let mut data = Arc::new(Mutex::new(vec![1; 10]));

    for x in 0..10 {
        let data_ref = data.clone();
        handles.push(thread::spawn(move || {
            let mut data = data_ref.lock().unwrap();
            data[x] += 1;
        }));
    }

    for handle in handles {
        let _ = handle.join();
    }

    dbg!(data);
}

これでエラーなく実行できる。

Mutexはlockというメソッドを提供する。これはMutexに複数のスレッドが同時にアクセスしないことを保証するためのメカニズム。 あるスレッドがdataへの参照を得ると、それ以外のスレッドはlockが完了しなくなる。 dataへの参照を得るのは常に1つのスレッドだけであることを保証する。これを排他制御という。

メッセージパッシング

メッセージパッシングとは各スレッドがメッセージをやり取りしながら動作する方法。 Rustはこのメッセージパッシングに対応したスレッド間通信用のチャンネルを持っている。

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    let handle = thread::spawn(move || {
        let data = rx.recv().unwrap();
        println!("{}", data);
    });

    let _ = tx.send("Hello, world");

    let _ = handle.join();
}

チャンネルはmpsc::channel関数で作成できる。その戻り値は送信・受信インスタンスのタプルとなっている。 このうち受信側のrxをスレッドに移動して、tx側から"Hello、World"というメッセージを送っている。 メッセージを送る際はsend、受け取る際はrecvを使う。

use std::sync::mpsc;
use std::thread;

fn main() {
    let mut handles = Vec::new();
    let mut data = vec![1; 10];
    let mut snd_channels = Vec::new();
    let mut rcv_channels = Vec::new();

    for _ in 1..10 {
        let (snd_tx, snd_rx) = mpsc::channel();
        let (rcv_tx, rcv_rx) = mpsc::channel();

        snd_channels.push(snd_tx);
        rcv_channels.push(rcv_rx);

        handles.push(thread::spawn(move || {
            let mut data = snd_rx.recv().unwrap();
            data += 1;
            let _ = rcv_tx.send(data);
        }));
    }

    for x in 0..10 {
        let _ = snd_channels[x].send(data[x]);
    }

    for x in 0..10 {
        data[x] = rcv_channels[x].recv().unwrap();
    }

    for handle in handles {
        let _ = handle.join();
    }

    dbg!(data);
}

これはスレッド間を安全に渡すことのできない型をチャンネルを通して送ろうとしているのでコンパイルエラーになる。

SendとSync

Sendを実装した型はその所有権をスレッドをまたいで送信できることを示す。 Sendはほとんどの型に実装されているが一部の型には実装されていない。先ほどRcを別のスレッドに送信しようとしてエラーになったのはこれが原因。

Syncは複数のスレッドから安全にアクセスできることを示す。例えばMutexはlockメソッドによる排他制御によって複数のスレッドから安全にアクセスできるのでSyncを実装している。

非同期処理

asyncとawaitが用意されており非同期処理ができる。

use futures::executor;

struct User {
    // 何かデータを持っている
}

struct UserId(u32);

struct Db {}

impl Db {
    sync fn find_by_user_id(&self, user_id: UserId) -> Option<User> {

    }
}

async fn find_by_user_id(db: Db, user_id: UserId) -> Option<User> {
    db.find_by_user_id(user_id).await
}

fn main() {
    executor::block_on(find_by_user_id(Db {}, UserId(1)))
}

Future

RustではFutureを理解しておくことでasyn/awaitの動作に理解が深まる。

Futureとは、ざっくり言うと「将来のどこかで処理が完了するタスク」 他の言語ではPromiseと呼ばれるケースもある。

もう少し具体的な表現で言うと「将来に値が確定する計算のステートマシンを抽象化したインターフェース」。

Futureトレイトは以下のシグネチャを持つ。pollというメソッドだけを持ち、Poll<Self::Output>を返す、という形になっている。

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}

Futureトレイトを実装したタスクが「将来的に値が確定する計算」となる。タスクが作成された時点ではまだ実行されておらず、ランタイムに乗った時点でスケジューリングされ実行される。その実行するかの判断にpoll() すなわちポーリングによってチェックされる。

チェックする主体はWaker(std::task::Waker)であり、poll関数の引数として渡されるContext内にラップされている。Poll::Pendingが返されると返されると、poll()はまた別のタスクが実行状態になるまで呼ばれず、他のタスクを実行する。

Poll::Readyが返されると、タスクが実行完了となりランタイムは実行完了状態に移る。これが「ステートマシンを抽象化したインターフェース」であることを表す。このポーリングを繰り返しながら実行するランタイムのことをexecutorと呼ぶ。

Futureはゼロコスト抽象化を達成している。各Futureはステートマシンとして表現されており、一箇所のヒープに割り当てされる。 ステートマシンの中にはIOイベントごとにひとつの状態を格納している。

このように、各タスクに対して決まったメモリで一箇所にしか割り当てしないため、余計なメモリ割り当てのコストがかからない。したがってFutureはゼロコスト抽象化を達成できている。

以下Futureの動作を実際に確認できるサンプル。cargo add futuresした後に実行できる。

use futures::{executor, future::join_all};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct CountDown(u32);

impl Future for CountDown {
    type Output = String;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<String> {
        if self.0 == 0 {
            Poll::Ready("Zero!!!".to_string())
        } else {
            println!("{}", self.0);
            self.0 -= 1;
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

fn main() {
    let countdown_future1 = CountDown(10);
    let countdown_future2 = CountDown(20);
    let cd_set = join_all(vec![countdown_future1, countdown_future2]);
    let res = executor::block_on(cd_set);
    for (i, s) in res.iter().enumerate() {
        println!("{}: {}", i, s);
    }
}

作成時に呼ばれたu32型の数値を1回poll()が呼ばれるごとにカウントダウンしていき、0になるまではWakerに「また呼んで」と言いつつ(wake_by_ref())Poll::Pendingを返し続け、0であればPoll::Readyとともに”Zero”という文字列を返しています。

main関数内ではこのCountDownタスクを2つ作成し、まとめてexecutor::block_on()メソッドに渡している。executor::block_on()は渡したFutureが完了になるまでブロックして待つメソッド。

async/await

簡単な例

use futures::executor;

async fn async_add(left: i32, right: i32) -> i32 {
    left + right
}

async fn somethig_great_async_function() -> i32 {
    let ans = async_add(2, 3).await;
    println!("{}", ans);
    ans
}


fn main() {
    executor::block_on(somethig_great_async_function());
}

Futureを使って以下のように記述することもできるがasync/awaitを使った方が処理の見通しがよくなるなどのメリットがある。

async fn somethig_great_async_function() -> impl Future<Output = i32> {
    async {
        let sns = async_add(2, 3).await;
        println!("{}", sns);
        sns
    }
}

複数のawaitが存在した場合、awaitごとに値を受け取る。

async関数はその都度awaitしない限り、中身の評価が走らない。

ムーブ

asyncブロックとクロージャにはmoveキーワードを使用できる。 async moveブロックは、変数がasync moveブロックでも生存できるようにその変数の所有権を移す。

fn move_to_async_block() -> impl Future<Output = ()> {
    let outside_variable = "this is output".to_string();

    async move {
        // moveキーワードを用いたことにより、変数の所有権をasyncブロックの中に移し、ブロック内でも使用できるようにした
        println!("{}", outside_variable);
    }
}

ライフタイム

スレッドを跨いでFutureを送りたくなった際、`staticライフタイムを用いる必要がある。

下記では所有権のない変数を引数に渡そうとしたためコンパイルエラーになる

fn some_great_function() -> impl Future<Output = i32> {
    let value: i32 = 5;
    send_to_another_thread_with_borrowing(&value)
    // コンパイルエラー
}

async fn send_to_another_thread_with_borrowing(x: &i32) -> i32 {
    // 何か別スレッドへ送る処理が書かれている想定
}

asyncブロック内にvalueを書き、その中でさらにsend_to_another_thread_with_borrowing関数を呼び出すようにしてみる。 valueの所有権を引き伸ばすことで対処する。

fn some_great_function() -> impl Future<Output = i32> {
    async {
        let value: i32 = 5;
        send_to_another_thread_with_borrowing(&value).await
    }
}

非同期ランタイム

非同期ランタイムは、非同期計算の実行環境を指す。非同期ランタイムには現在いくつか種類があるが代表的なクレートは下記の2つがある。

tokio

Rustをずっと支えてきた非同期ライブラリで多くのRustライブラリが現在もtokioに依存した実装。

使うにはCargo.tomlに以下追加してcargo build実行。 macrosフィーチャを追加して#[tokio::main]というアトリビュートを利用可能にしている。 これによりmain関数をasync化できる。

tokio = { version = "0.2.21", features = ["macros"] }

以下のようにプログラムを書ける。

async fn add(left: i32, right: i32) -> i32 {
    left + right
}

#[tokio::main]
async fn main () {
    let ans = add(2, 3).await;
    println!("{}", ans);
}

async-std

近年登場してきたクレート。tokioは内部実装やAPIが少々複雑という問題点があり、もう少し直感的でわかりやすい非同期ランタイムを作ろうというモチベーションのもと、発足した。

使うにはCargo.tomlに以下追加してcargo build実行。

async-std = { verion = "1.6.1", features = ["attributes"] }

以下のようにプログラムを書ける。

async fn add(left: i32, right: i32) -> i32 {
    left + right
}

#[async_std::main]
async fn main () {
    let ans = add(2, 3).await;
    println!("{}", ans);
}

async-trait

Rustでは現状トレイトの関数にasyncをつけることができない。つまり下記のようなコードはコンパイルエラーになる。

trait AsyncTrait {
    async fn f() {
        println!("Couldn't compile");
    }
}

使うには以下実行

cargo add async-trait

トレイトに#[async_trait]というアトリビュートをつけると、async fn..という関数宣言をできるようになる。

use async_trait::async_trait;

#[async_trait]
trait AsyncTrait {
    async fn f() {
        println!("Could compile");
    }
}

もしトレイトの中身は実装先に委ねたい場合は実装先(impl)にも#[async_trait]アトリビュートを追加することで、async fnという関数宣言を同様にできるようになる。

#[async_trait]
trait AsyncTrait {
    async fn f(&self);
}

struct Runner {}

#[async_trait]
impl AsyncTrait for Runner {
    async fn f(&self) {
        println!("Hello, async-trait");
    }
}