0Pricing
Learn Rust Coding · レッスン

スコープ付きスレッド

スレッド間で借用します

「スコープ付きスレッド」はCoddyKit上の無料Learn Rust Codingレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLearn Rust Coding学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Learn Rust Codingコースには全4レッスンが含まれています。

スレッドで借用するときの問題

通常のthread::spawnクロージャは'staticでなければなりません。スレッドが、それを所有する関数より長く存続する可能性があるため、ローカル変数を借用できないのです。そのため、moveやArcをよく目にします。

スコープ付きスレッドは、すべてのスレッドがスコープの終了前に完了することを保証することで、この問題を解決します。そのため、ローカルデータを安全に借用できます。

spawnに'staticが必要な理由

thread::spawnでは、生成したスレッドがmainや任意の関数の戻り値より後まで実行し続ける可能性があります。ローカル変数を借用していた場合、スレッドがまだ使用している間にその変数が破棄されるかもしれません。Rustはこれをコンパイル時に禁止し、所有するデータをクロージャにmoveするよう要求します。

use std::thread;

fn main() {
    let nums = vec![1, 2, 3];
    // move transfers ownership into the thread
    let handle = thread::spawn(move || {
        println!("in thread: {:?}", nums);
    });
    handle.join().unwrap();
}

thread::scopeの紹介

Rust 1.63で安定化されたstd::thread::scopeは、スレッドがローカル変数を借用できるスコープを作成します。スコープは内部のすべてのスレッドが完了するまでブロックするため、借用がダングリングすることはありません。

thread::spawnの代わりにスコープハンドルsを使い、s.spawn(...)でスレッドを生成します。

use std::thread;

fn main() {
    let data = vec![10, 20, 30];
    thread::scope(|s| {
        s.spawn(|| {
            println!("borrowed: {:?}", data);
        });
    });
    // data is still usable here
    println!("after scope: {:?}", data);
}

moveなしで借用する

thread::scopeの内部では、moveなしでローカル変数を参照によって読み取れます。通常の参照と同じように、複数のスコープ付きスレッドが同じデータを同時に不変借用できます。

use std::thread;

fn main() {
    let message = String::from("shared text");
    thread::scope(|s| {
        s.spawn(|| println!("thread 1 sees: {}", message));
        s.spawn(|| println!("thread 2 sees: {}", message));
    });
    println!("main still owns: {}", message);
}

スライスに分割して処理する

よくあるパターンは、スライスを分割して各スレッドにチャンクを処理させることです。スコープ付きスレッドを使えば、各スレッドが元のスライスの一部を直接借用できるため、cloneする必要がなく、すっきり記述できます。

use std::thread;

fn main() {
    let numbers = [1, 2, 3, 4, 5, 6];
    let (left, right) = numbers.split_at(3);
    thread::scope(|s| {
        s.spawn(|| {
            let sum: i32 = left.iter().sum();
            println!("left sum:  {}", sum);
        });
        s.spawn(|| {
            let sum: i32 = right.iter().sum();
            println!("right sum: {}", sum);
        });
    });
}

戻り値を集める

通常のスレッドと同様に、s.spawnはScopedJoinHandleを返します。.join()を呼び出すと、スレッドの戻り値を取得できます。ハンドルを集めておき、生成後にjoinすれば結果をまとめて取得できます。

use std::thread;

fn main() {
    let inputs = [2, 4, 6];
    let mut handles = vec![];
    thread::scope(|s| {
        for &x in &inputs {
            handles.push(s.spawn(move || x * x));
        }
        let results: Vec<i32> = handles.into_iter()
            .map(|h| h.join().unwrap())
            .collect();
        println!("{:?}", results);
    });
}

可変借用には注意が必要

2つのスコープ付きスレッドが、同じデータを同時に可変借用することはできません。それではRustのエイリアシング規則に違反するためです。複数のスレッドから共有データを変更するには、引き続きMutexが必要です。ただし、1つのスレッドであれば、互いに重ならない部分を一意に可変借用できます。

以下では、split_at_mutを使って各スレッドが配列の別々の半分を変更します。

use std::thread;

fn main() {
    let mut data = [1, 2, 3, 4];
    let (a, b) = data.split_at_mut(2);
    thread::scope(|s| {
        s.spawn(|| { for x in a.iter_mut() { *x *= 10; } });
        s.spawn(|| { for x in b.iter_mut() { *x += 100; } });
    });
    println!("{:?}", data);
}

スコープが自動的にjoinする

スコープ付きスレッドごとにjoinを呼び出す必要はありません。scopeクロージャが戻ると、Rustは続行する前に、まだjoinされていないすべてのスレッドを自動的にjoinします。これにより、スレッドの存続期間全体で借用が有効であることが保証されます。

use std::thread;
use std::time::Duration;

fn main() {
    let label = String::from("task");
    thread::scope(|s| {
        s.spawn(|| {
            thread::sleep(Duration::from_millis(30));
            println!("{} done", label);
        });
        println!("spawned, scope will wait");
    });
    println!("all scoped threads finished");
}

スコープと共有変更の組み合わせ

スレッドが同じ値を変更する必要がある場合は、スコープ付きスレッドとMutexを組み合わせます。スコープによってスレッドがローカルなMutexを直接借用できるため、Arcは必要ありません。

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    thread::scope(|s| {
        for _ in 0..5 {
            s.spawn(|| {
                let mut n = counter.lock().unwrap();
                *n += 1;
            });
        }
    });
    println!("counter = {}", *counter.lock().unwrap());
}

スコープ付きと通常のスレッドの使い分け

処理の範囲が限定されていて関数内で完了し、Arcやcloneを使わずにスタック上のデータを借用したい場合は、スコープ付きスレッドを使います。

スレッドを現在の関数より長く存続させる必要がある場合や、プログラムの存続期間全体にわたって実行する場合は、thread::spawnを使います。スコープ付きスレッドはスコープの外へ出られません。

並列mapの例

ここまでの内容をまとめて、ベクターの各要素を入力を借用しながらそれぞれ別のスレッドで変換し、結果を順番どおりに集める、小さな並列mapを作ります。

use std::thread;

fn parallel_double(items: &[i32]) -> Vec<i32> {
    let mut handles = Vec::new();
    let mut out = Vec::new();
    thread::scope(|s| {
        for &x in items {
            handles.push(s.spawn(move || x * 2));
        }
        for h in handles {
            out.push(h.join().unwrap());
        }
    });
    out
}

fn main() {
    let nums = vec![1, 2, 3, 4];
    println!("{:?}", parallel_double(&nums));
}

理解度チェック

スコープ付きスレッドの理解度を確認しましょう。

まとめ

スコープ付きスレッドについて学びました。

  • thread::spawnには'staticクロージャが必要ですが、スコープ付きスレッドには必要ありません。
  • thread::scopeを使うと、スレッドからローカル変数を安全に借用できます。
  • スコープは戻る前にすべてのスレッドを自動的にjoinします。
  • 可変共有には引き続きMutexが必要ですが、スコープ内ではArcは必要ありません。
  • 範囲が限定された、関数内の並列処理にはスコープ付きスレッドを使います。

よくある質問

「スコープ付きスレッド」レッスンは無料ですか?

はい。「スコープ付きスレッド」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Learn Rust Codingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Learn Rust Codingコースには全4レッスンが含まれています。

「スコープ付きスレッド」で何を学びますか?

スレッド間で借用します ブラウザで直接実行するハンズオンコードでLearn Rust Codingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Learn Rust Codingを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのLearn Rust Codingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「スコープ付きスレッド」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このLearn Rust Codingレッスンでコードを書いて実行できますか?

はい。すべてのLearn Rust Codingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. mpscチャネル
  2. Arc/Mutexによる状態共有
  3. スコープ付きスレッド
  4. Crossbeamチャネル
← Learn Rust Codingに戻る