質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

Q&A

解決済

1回答

133閲覧

Rustにて、Arc<T>がSendになるためには、TがSendとSyncの両方を実装してなければならないのは何故ですか?

tasuren

総合スコア77

Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

0グッド

0クリップ

投稿2025/05/09 10:46

RustのArc<T>は、Sendを満たすためにTがSendとSyncの両方を満たさなければならないと、ドキュメントに書いてありました。
なぜSendの場合はTがSendのみ、Syncの場合はTがSyncのみ実装していれば良い、というわけではないのでしょうか?私は自分なりに考えたところ、RefCellといったSendのみ実装するものをArcでラップした際に問題が生じるからではないか、という結論となりましたが、これであってますでしょうか?ご教授お願いしたいです。

私は自分なりに考えたところ、以下のコードを書いてみました。

rust

1fn main() { 2 // RefCellをArcでラップする。 3 let cell = std::sync::Arc::new(std::cell::RefCell::new(42)); 4 5 let handle = std::thread::spawn(move || { 6 // cellの所有権を移動して、その値を使う。 7 println!("{}", cell.borrow()); 8 }); 9 10 handle.join().unwrap(); 11}

このコードでは、RefCellをArcでラップしてメインスレッドではないスレッドでRefCellの中の値42の参照を作ろうとしています。
ここで、Syncを実装していないということは、&TがSendではないということです。その上で、それにも関わらず、このコードではArcのDerefトレイトによりRefCellの参照がcell.borrow()にて作られてしまっていて、メインスレッドではない場所でRefCellへのアクセスが発生してしまっています。そのため、このコードはコンパイルが通ってはいけないと私は考えました。
この時、cellの値であるArc<RefCell<i32>>がSendではないので実際にもコンパイルは通らないのですが、それはArc<T>がSendを満たすためにSendとSyncがTに実装されていないといけないがために、このコンパイルエラーが実現した、と考察できます。
つまり、RefCellなどのSendのみ実装するものをArcでラップした際に問題が発生するため、Arc<T>がSendになるにはTがSend + Syncである必要がある、と考えました。

私の質問は、なぜArc<T>がSendを満たすにはTがSendとSyncの両方を満たしてなければならないのか、というものでした。そこで自分なりに考えたところ、RefCellといったものをArcでラップしてスレッド間の移動をした際に問題が生じてしまうから、という結論がでました。私の質問への答えは、これであってますでしょうか?それとも、他が理由でしょうか?

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

大筋の方針としてはそんな感じで良いかと思います。ただ、補足するならば、

メインスレッドではない場所でRefCellへのアクセスが発生してしまっています。

この部分は「メインスレッドではない場所」からアクセスできることが問題であるというよりは、メインスレッドを含む「複数のスレッドから同時にアクセスが発生しうる」のが問題であるという表現が正確でしょう。

例えば、Arcで包まずにRefCellをそのまま他スレッドにムーブした場合にも「メインスレッドではない場所」からメインスレッドで作成したRefCellへのアクセスが発生しますが、これはRefCell型の実装するSendトレイトの保証の範囲内の操作であり、特に問題にはなりません。一方で、ご例示のコードで問題になるのは、cloneによって同じ中身を指す参照を複製できるというArcの性質により、spawnしたスレッドにArcをムーブする前にその複製を作っておくことで、元のスレッドとムーブ先のスレッドから同時に参照を共有できてしまうことです。これをコードで表すと次のようになります:

rust

1use std::cell::RefCell; 2use std::sync::Arc; 3use std::thread; 4 5fn bad() { 6 let arc = Arc::new(RefCell::new(42)); 7 // 中身の参照を保持したまま複製した`Arc`を他スレッドに送れる 8 let _cell: &RefCell<_> = &arc; 9 let cloned = arc.clone(); 10 thread::spawn(move || { 11 //~^ ERROR `RefCell<i32>` cannot be shared between threads safely 12 let _cell: &RefCell<_> = &cloned; // 元のスレッドと同時に`RefCell`にアクセスできてしまう 13 }); 14}

(Playground)

(注:ご例示のコードではArccloneしていないので実際にはスレッドを跨いだ参照の共有は発生していませんが、コンパイラは実行時におけるArcの参照カウントの動きを追ったりは出来ないので(そもそもコンパイラが生存期間を追跡できる&TでカバーできないユースケースのためにArc<T>のような型が存在するのだと考えれば自然なことでしょう)、コンパイラの視点からするといずれも同様に問題のあるコードに見えるのです。)

ちなみに問題のない例(Arcで包まずにムーブする場合とArcの中身がSyncを実装する場合)は次のようになります:

rust

1use std::cell::RefCell; 2use std::sync::Arc; 3use std::thread; 4 5fn ok1() { 6 let cell = RefCell::new(42); 7 thread::spawn(move || { 8 // `RefCell<_>: Send`なので`Arc`で包まずにムーブするだけなら問題なし 9 let _ = cell; 10 }); 11} 12 13fn ok2() { 14 // `Sync`を実装する型の場合: 15 let arc = Arc::new(42); 16 let _borrow: &u32 = &arc; 17 let cloned = arc.clone(); 18 thread::spawn(move || { 19 let _borrow: &u32 = &cloned; // `u32: Sync`なので問題なし 20 }); 21}

(Playground)


ちなみに、ご質問では具体的に問題になる例を作ることで説明されましたが、これをもう少し抽象化して雑に説明するならば「Arc<T>&Tと同様の働きを持つから、&T: Sendを満たすためにT: Syncが必要であるのと同様にArc<T>: Sendを満たすのにT: Syncが必要」と考えてみると覚えやすいかと思います(ここでいう「同様の働き」というのはつまり、&Arc<T>から&Tが得られるということと、前述の通り参照の同一性を保ったままArccloneできるという性質です)。

投稿2025/05/10 13:12

tesaguri

総合スコア44

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tasuren

2025/05/11 01:08 編集

抽象化した最後の説明がとても簡潔です。とても納得しました。 回答ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問