【ITニュース解説】Rust Send and Sync in Simple terms
2025年09月25日に「Dev.to」が公開したITニュース「Rust Send and Sync in Simple terms」について初心者にもわかりやすく解説しています。
ITニュース概要
Rustの`Send`と`Sync`は、マルチスレッドでのデータ安全性を保証する特性だ。`Send`はデータの所有権をスレッド間で安全に移動できるか、`Sync`は参照を共有できるかを示す。これにより、並行処理で起こる競合状態などを防ぐ。`Rc`などはスレッド非安全で、`Arc`や`Mutex`などで対応する。
ITニュース解説
Rustでマルチスレッドのプログラムを作成する際、error[E0277]というエラーに遭遇することがある。このエラーメッセージは、「YourTypeはスレッド間で安全に送信できない」といった内容で、特に「SendトレイトがSomeInnerTypeに実装されていない」という具体的な理由が示されることが多い。このようなエラーは、Rustがスレッドセーフティを厳密にチェックしていることを示しており、この問題を理解し解決するためには、SendとSyncという二つの重要なトレイトを理解する必要がある。
SendとSyncは、Rustにおける「トレイト」の一種だ。トレイトとは、特定の型がどのような振る舞いや能力を持っているかを定義する仕組みで、他の言語のインターフェースやポリモーフィズムに近い概念である。しかし、SendとSyncは単なるトレイトではなく、「マーカートレイト」と呼ばれる特殊な種類に分類される。マーカートレイトは、メソッド(関数)を一切持たないのが特徴で、その唯一の目的は、コンパイラに対して型が特定の「状態」や「性質」を持っていることをマークすることにある。コンパイル時にこれらのマークを利用して、プログラムが特定の基準を満たしているか、特にスレッドセーフティに関して安全であるかを検証するのだ。RustにはSizedやUnpinといった他のマーカートレイトも存在する。
さらに、SendとSyncは「オートトレイト」でもある。オートトレイトとは、ある型を構成するすべての内部的な型(フィールドなど)がそのトレイトを実装している場合、その型自体も自動的にそのトレイトを実装するという性質を持つ。この自動的な実装は、開発者が手動でSendやSyncを実装する手間を省き、かつ型がスレッドセーフであるかどうかの推論を容易にする。
では、SendとSyncが具体的に何を意味するのかを詳しく見てみよう。Sendトレイトは、ある型の「所有権」をスレッド間で安全に移動できることをコンパイラに伝える役割を持つ。つまり、あるスレッドで作成した変数を、その所有権ごと別のスレッドに渡して、その別のスレッドで変数を使用できるかどうかを判断する基準となる。一方、Syncトレイトは、ある型の「共有参照」(&T)をスレッド間で安全に共有できることを示す。これは、複数のスレッドが同時に同じデータへの不変な参照を持つことが可能かどうかを判断するためのものだ。もし型がSendもSyncも実装していれば、その型の所有権をスレッド間で移動することも、その型への参照をスレッド間で共有することも安全に行えることになる。
Rustの基本的な型、いわゆるプリミティブ型は、ほとんどがSendとSyncの両方を自動的に実装している。例えば、i8やu32のような整数型、f32やf64のような浮動小数点数型、bool、char、そしてユニット型である()などがこれにあたる。これらの型はスレッド間で安全に移動したり共有したりできる。また、配列やタプルも、その内部の要素が全てSendとSyncであれば、自動的に両方のトレイトを実装する。しかし、例外も存在する。例えば、一部の生ポインタは使い方によってはSendになり得るが、通常はSendでもSyncでもなく、非常に慎重な扱いが必要だ。また、参照カウントスマートポインタであるRc<T>や、内部可変性を提供するUnsafeCell<T>も、デフォルトではSendでもSyncでもない。これらの型の特性を理解することは、安全な並行プログラミングにおいて非常に重要となる。
SendとSyncの違いを具体的なコードで確認してみよう。まず、String型の値を二つのスレッドで共有する例を考える。thread::scopeを使ってスコープスレッドを作成し、その内部でString型の変数valueを定義する。二つのスレッドを生成し、それぞれがvalueの参照をdbg!マクロでデバッグ出力しようとすると、これは問題なくコンパイルできる。String型はSyncであるため、複数のスレッドがその不変参照を安全に共有できるからだ。次に、一方のスレッドでvalueの所有権を移動させてみよう。s.spawn(move || { dbg!(value) });のようにmoveキーワードを使用すると、valueの所有権がそのクロージャ(スレッドの処理内容)に完全に渡される。String型はSendであるため、所有権の移動も安全に行え、このコードもコンパイルが通る。このことから、String型がSendとSyncの両方を実装していることがわかる。
ここで一つ注意すべき点として、i32のようなCopyトレイトを実装している型の場合、moveキーワードを使っても所有権の移動ではなく値のコピーが行われる。そのため、let value = 0;とした場合、moveキーワードを使って一方のスレッドにvalueを渡しても、元のvalueは依然として存在し、別のスレッドからその参照を共有することも可能となる。これはCopyセマンティクスによるもので、SendやSyncとは別の性質が関わっている。
次に、SendもSyncも実装しない型の例とその理由を見ていこう。これらのトレイトは「スレッドセーフティ」を保証するために存在する。複数のスレッドが並行して動作する環境では、共有データに対する操作が「アトミック」である必要がある。アトミックな操作とは、他のスレッドから見て中断されることなく、一つの不可分な単位として完了する操作のことだ。もし型が非アトミックな方法で内部のデータを変更できる場合、複数のスレッドが同時にそのデータにアクセスしようとすると、「競合状態」(レースコンディション)が発生したり、「未定義動作」(プログラムのクラッシュや予期せぬ結果)を引き起こす可能性があり、これは非常に危険だ。このような型はSyncとして安全ではないと判断される。同様に、非アトミックな参照カウントなどの方法で複数の所有者がデータを管理する型は、所有権の移動が安全でないと判断され、Sendとして扱われない。
例えば、std::cell::RefCellは「内部可変性」を提供する型だが、これはスレッドセーフではない。RefCellは、単一スレッド内での不変参照から可変データを変更することを可能にするが、複数のスレッドが同時にRefCellを通じてデータを変更しようとすると、競合状態が発生する危険がある。そのため、RefCell<T>はSyncトレイトを実装しておらず、RefCellの参照をスレッド間で共有しようとするとエラーが発生する。この問題を解決するには、std::sync::Mutexやstd::sync::RwLockのような「ミューテックス」を利用する必要がある。これらはアトミックな操作によってデータを保護し、一度に一つのスレッドだけがデータにアクセスできるようにすることでスレッドセーフティを保証する。Mutex<T>はSyncを実装しているため、その参照を複数のスレッドで共有しても安全だ。
さらに、複数のスレッドからデータにアクセスするために参照カウントが必要な場合を考えてみよう。std::rc::Rc<T>は参照カウントを行うスマートポインタだが、これは非アトミックな参照カウントを用いるため、スレッドセーフではない。Rc<T>のクローンを作成して参照カウントを増やしたり減らしたりする操作は、複数のスレッドが同時に実行すると競合状態を引き起こす可能性がある。そのため、Rc<T>はSendでもSyncでもない。Rc<T>を含んだ型をスレッド間で移動したり共有したりしようとすると、コンパイルエラーとなる。この問題を解決するためには、スレッドセーフな参照カウントスマートポインタであるstd::sync::Arc<T>を使用する必要がある。Arc<T>は、参照カウントの増減にアトミック操作を使用するため、複数のスレッドから安全に参照カウントを管理できる。したがって、Arc<Mutex<T>>のようにArcとMutexを組み合わせることで、複数のスレッド間で安全に所有権を共有し、かつ内部のデータを可変に扱うことが可能になる。
まとめると、RustのSendとSyncトレイトは、マルチスレッド環境におけるデータアクセスの安全性を保証するための非常に重要な仕組みだ。Sendは型の所有権がスレッド間で安全に移動できることを示し、Syncは型の共有参照がスレッド間で安全に共有できることを示す。これらのトレイトはマーカートレイトでありオートトレイトであるため、コンパイラが自動的にスレッドセーフティを検証してくれる。非スレッドセーフな内部可変性を持つRefCellや、非アトミックな参照カウントを行うRcなどは、SendやSyncを実装しない。これらを使用する場合は、MutexやArcといったスレッドセーフな代替手段を用いることで、競合状態や未定義動作といった危険なバグを未然に防ぎ、堅牢な並行プログラムを構築できるのだ。この知識を基に、より安全で効率的なRustのマルチスレッドプログラミングを実践できるだろう。