Send と Sync

このページの目次

Send は、ある型の値がスレッド間で安全に移動できることを示し、Sync は共有参照がスレッド間で安全に渡せることを示します。これらはコンパイル時の契約であり、実行時に自動的にロックを追加するものではなく、内部が完全に不変である必要もありません。

形式的には、T: Sync であるための必要十分条件は &T: Send です。この定義により、「オブジェクトを共有する」ことは「その共有参照を別のスレッドに送る」ことに帰着されます。Send、Sync

スレッドA:Tを所有値を別スレッドに渡すスレッドB:Tを取得T: Send が必要Tを共有複数のスレッドが &T を保持&T をスレッド間で渡送T: Sync が必要(つまり &T: Send)2つのスレッド間使用方法:値の移動と共有参照の渡送これらのトレイトは対応するアクセス方法を許可しますが、ビジネスロジックの原子性、デッドロックの回避、低レイテンシを保証するものではありません。

一般的な型の条件

以下の表では、一般的なデフォルトのアロケータを持つ型について、一部のジェネリクスとライフタイムの詳細を省略して説明します。

型Send の条件Sync の条件理由
Vec、BoxT: SendT: Sync所有権と借用が内部要素へ伝播する
Cell、RefCellT: SendSync を実装しない排他的に移動できるが、任意の並行共有アクセスは不可
RcSend を実装しないSync を実装しない非アトミックな参照カウントにより、他のクローンと共有アクセスされる可能性がある
ArcT: Send + SyncT: Send + Sync参照カウントはアトミックだが、T の同期は自動的に行われない
MutexT: SendT: Sendロックにより内部アクセスを直列化する
RwLockT: SendT: Send + Sync共有読み取りロックにより、複数のスレッドが T を同時に読み取れる
*const T、*mut Tデフォルトで実装しないデフォルトで実装しない裸のポインタではリソースとスレッドの契約を自動的に証明できない

RefCell<u32> は Send だが Sync ではない。これは、両者が同じ「スレッドセーフスイッチ」ではないことを示しています。Arc<RefCell<u32>> も、Arc を追加しただけではスレッド間で共有できません。Arc は所有権カウントのみを管理し、RefCell の借用状態は同期されません。

Mutex<T> の Sync は T: Send のみを要求します。これは、ロックにより異なるスレッドが T を順次排他的にアクセスできるためであり、同時に共有読み取りできるためではありません。std::sync::MutexGuard は、あるプラットフォームでは同じスレッドで解放する必要があるロックを、別のスレッドで解放することを避けるため、プラットフォーム間の移植性を考慮して Send を実装していません。これは Mutex 自体の条件と矛盾しません。

RefCell の移動と、RefCell への共有参照の共有

use std::cell::RefCell;
use std::thread;
fn main() {
    let local = RefCell::new(vec![1, 2]);
    let returned = thread::spawn(move || {
        local.borrow_mut().push(3);
        local
    }).join().unwrap();
    assert_eq!(&*returned.borrow(), &[1, 2, 3]);
}

メインスレッドは RefCell の所有権をワーカースレッドに渡し、作業終了後に返します。2つのスレッドが動的な借用状態を同時に操作しないため、RefCell<Vec<i32>> は Send を満たすことができます。もし &local を別のスレッドに渡し、元のスレッドでアクセスを維持する場合、RefCell が Sync を満たす必要がありますが、それは満たしていません。

Rc の制限はさらに厳格です。たとえ実行時に強い参照が1つだけあったとしても、Rc<T> の型がそれによって一時的に Send を実装することはありません。コンパイル時の制約は、1回のカウントクエリから長期的な所有権の証明を得ることはできません。一意の時点で T を取り出して渡すか、あるいはスレッド間で共有する必要がある場合は、条件を満たす Arc を選択します。

Mutex が非 Sync な T を包める理由

use std::cell::RefCell;
use std::sync::Mutex;
use std::thread;
fn main() {
    let value = Mutex::new(RefCell::new(0u32));
    thread::scope(|scope| {
        for _ in 0..2 {
            let shared = &value;
            scope.spawn(move || {
                let guard = shared.lock().unwrap();
                *guard.borrow_mut() += 1;
            });
        }
    });
    assert_eq!(value.into_inner().unwrap().into_inner(), 2);
}

Mutex は RefCell へのアクセスをスレッドが順次クリティカルセクションに入るように保証するため、ここでは RefCell 自体が Sync である必要はありません。この組み合わせはトレイトの導出を演示するためのものです。実際、このカウンターには Mutex<u32> だけで十分です。MutexGuard の排他的アクセスを取得している場合、さらに RefCell でラップすることは通常不要です。

RwLock の読み取りガードは、複数のスレッドが同時に内部の共有参照を取得することを許可するため、その Sync 条件では T: Sync も要求されます。「ロックを1つ持っているから」という理由で、すべてのロック型が同じ条件を満たすわけではありません。

Send と Sync を層ごとに導く

RefCell を Arc と Mutex に入れて Sync を比較します。重要なのは外側が T にどうアクセスするかで、Arc は共有アクセス、Mutex はスレッド間の独占アクセスを直列化します。Rc に変えると、T 自体が Send でない別の制限も現れます。

図を準備しています
Send と Sync を層ごとに導く

Arc は非 Sync の値を共有可能にはせず、Mutex の Sync は内部の T: Send を要求します。

コンパイラによる導出チェック

use std::cell::RefCell;
use std::sync::{Arc, Mutex};

fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}

fn main() {
    assert_send::<RefCell<u32>>();
    assert_send::<Arc<String>>();
    assert_sync::<Arc<String>>();
    assert_sync::<Mutex<RefCell<u32>>>();
    // assert_sync::<RefCell<u32>>(); // Sync を満たさない
    // assert_send::<Arc<RefCell<u32>>>(); // Arc は内部の同期を補完しない
}

自動トレイトの構造導出はデフォルトのルールですが、型自体の明示的な実装などによっても影響を受けます。「裸のポインタ、Rc、いくつかの Cell を除き、他のすべての型が必ず Send/Sync である」というわけではありません。プラットフォームハンドル、スレッド関連オブジェクト、サードパーティの抽象化にも制限がある可能性があります。

Send と 'static は異なる軸

通常の thread::spawn は通常、クロージャが Send + 'static であることを要求します。Send はスレッド間での使用を管理し、'static はキャプチャされた借用が独立したスレッドよりも早く期限切れにならないことを管理します。'static は、その値が永遠に生存し続けることを意味するわけではありません。

String をクロージャに move することで、短い借用がない条件を満たすことができます。&str を move すると、参照のみが移動し、参照先のオブジェクトの寿命は延長されません。スコープの終了前に確実に完了するスレッドの場合、thread::scope を使用して外側のデータを借用できます。詳しくは スレッドと同期 を参照してください。

async Future が Send かどうかは、それが保持する可能性のある状態によって同様に決まります。Send ではない値がポイントの跨ぎ(await)中に生存すると、通常、Future をスレッド間でスケジューリング可能な spawn に渡すことができなくなります。エラーを消去するために安易に unsafe impl Send を行ってはいけません。値やロックガードのスコープを狭めるか、必要に応じてローカルなタスク実行メカニズムを使用します。

スレッドスコープはライフタイムを緩和するが、Send/Sync は緩和しない

use std::thread;
fn main() {
    let mut values = [1, 2, 3, 4];
    thread::scope(|scope| {
        let (left, right) = values.split_at_mut(2);
        scope.spawn(move || { for value in left { *value *= 10; } });
        scope.spawn(move || { for value in right { *value += 1; } });
    });
    assert_eq!(values, [10, 20, 4, 5]);
}

split_at_mut は重複しない領域を確立し、スレッドはそれぞれ排他的な参照を取得します。scope は、これらのスレッドが終了するまでスコープを抜けないことを保証するため、ローカル配列を借用できます。配列全体をコピーする必要はなく、Arc で所有関係を延長する必要もありません。スレッドスコープ

ただし、scope は Rc を Send にするものではなく、&RefCell<T> をスレッド間で安全に共有できるものにもしません。それは借用の有効性を維持できるかどうかの問題を解決するものであり、共有アクセスの同期問題を解決するものではありません。

移動されるもの必要な核心条件
TT: Send
&TT: Sync
&mut TT: Send;元のスレッドはこの排他的借用を同時に使用できない
借用を含むクロージャキャプチャされた実際のフィールドがスレッド要件を満たし、借用がスレッドの実行期間をカバーする

Future の状態はポイントと初期キャプチャの両方を見る

use std::rc::Rc;
fn assert_send<T: Send>(_: T) {}
async fn local_work() {
    {
        let local = Rc::new(String::from("temporary"));
        assert_eq!(local.len(), 9);
    }
    std::future::ready(()).await;
}
fn main() {
    assert_send(local_work());
}

ここでは、Rc は async 関数が最初に poll された後に作成され、await の前に使用が終了して解放されるため、Future は Send を満たすことができます。この例は Future の型のみをチェックしており、実行器は実行していません。関数体内のアサーションは、poll された場合にのみ実行されます。

Rc が外部から async move でキャプチャされている場合、Future は最初の poll の前にそれを所有することになります。関数体が最初の await の前にドロップする準備をしていても、「await を跨がないから必ず Send」と単純に適用することはできません。診断時には、初期キャプチャ、ポイント時に保存されたローカル値、および戻り値の要件を同時に確認します。具体的なランタイムでの spawn では、Future::Output: Send も要求される場合があり、これは Future 自体の Send とは別の制約です。

PhantomData は論理的関係を表現し、同期を提供しない

use std::marker::PhantomData;
use std::rc::Rc;

struct ThreadBound {
    handle: usize,
    _thread_bound: PhantomData<Rc<()>>,
}
fn main() {
    let resource = ThreadBound { handle: 7, _thread_bound: PhantomData };
    assert_eq!(resource.handle, 7);
}

handle が単なる整数であっても、マーカは、このラップされた型が自動的に Send/Sync であってはならないことを表現します。PhantomData は実際の Rc を保持しませんが、自動トレイト、分散性(variance)、およびデストラクタチェックに関連する導出に参加します。PhantomData の型を変更すると、複数の意味が同時に変わる可能性があり、スレッド属性のボタンを1つ押すような単純なものではありません。

逆に、PhantomData<T> を追加しても、裸のポインタを含むコンテナが自動的に Send を証明するわけではありません。unsafe impl を手動で記述する必要がある場合は、移動/共有のすべての公開操作、コールバック、およびデストラクタがスレッド契約に従っていることを証明する必要があります。「基盤ライブラリがたぶんスレッドセーフだから」という理由では不十分です。

エントリポイントの要件spawn:Send と寿命の制約クロージャ・Future の捕捉捕捉:T / &T / Arc<T>具体的なフィールドカウント・借用・スレッド制約所有関係の調整、借用範囲の縮小、または適切な同期プロトコルの選択スレッド制約エラーが発生した場合、実際に渡送される型に沿って段階的に特定する修正はモデル層で行う;unsafe impl は証明の義務であり、エラーを消去するためのスイッチではないコンパイラが報告する最終的な非 Send フィールドが最も価値があることが多い;外側のエラー位置が必ずしも設計上の問題の原因であるとは限らない。

unsafe impl はデストラクタとエイリアスをカバーする必要がある

外部ハンドルをラップする場合、すべてのメソッドがロックを持っていても、基盤が作成元のスレッドでそのハンドルを破棄することを要求している場合、所有者を任意のスレッドに移動できるようにすることは契約に違反する可能性があります。同様に、あるメソッドが同期を回避する内部ポインタを返す場合、またはコールバックがロックの外で同じ状態にアクセスできる場合、一見保護されているモデルを破壊します。

誰がリソースを所有し、どのエイリアスが許容され、どのスレッドがデストラクタを実行し、すべての公開操作が同じルールを維持しているかを項目ごとに確認する必要があります。基盤の API がスレッドバインドされている場合、タスクをそのハンドルを所有するスレッドに送り返すか、明確な非 Send のラップを保持します。手動で unsafe impl を記述するには、これらのパスと一致する証明が必要であり、「Mutex を使用している」というだけでは不十分です。

コンパイルできるからといって、並行ビジネスロジックが正しいわけではない

Arc<Mutex<アカウント>> は同期されていないデータアクセスを回避できますが、2つのアカウントにまたがる操作を原子トランザクションにするものではなく、ロック順序によるデッドロックを排除するものでもありません。Sync は内部可変性に対する安全な同期を要求しますが、各グループの多段階操作がビジネス不変条件を満たすことを保証するものではありません。

インターフェースを設計する際は、まず共有、移動、またはメッセージパッシングのいずれを使用するかを決定し、トレイトの条件が実際のモデルを反映させるようにします。Send/Sync のエラーが発生することは、通常、リソースまたはライフタイムの決定がまだ明確になっていないことを示しています。