0Pricing
Learn Rust Coding · บทเรียน

เธรดตามขอบเขต

ยืมข้อมูลข้ามเธรด

เธรดตามขอบเขต เป็นบทเรียน Learn Rust Coding ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Learn Rust Coding และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Learn Rust Coding มีบทเรียนทั้งหมด 4 บทเรียน

ปัญหาของการยืมข้อมูลในเธรด

คลอเชอร์ของ thread::spawn ปกติต้องเป็น 'static: ไม่สามารถยืมตัวแปรภายในได้ เพราะเธรดอาจทำงานต่อหลังจากฟังก์ชันที่เป็นเจ้าของตัวแปรเหล่านั้นสิ้นสุดลง นั่นจึงเป็นเหตุผลที่คุณมักเห็น move และ Arc

เธรดแบบมีขอบเขตแก้ปัญหานี้ด้วยการรับประกันว่าเธรดทุกเธรดจะทำงานเสร็จก่อนขอบเขตสิ้นสุด ทำให้การยืมข้อมูลภายในปลอดภัย

เหตุใด spawn จึงต้องใช้ 'static

เมื่อใช้ thread::spawn เธรดที่สร้างขึ้นอาจทำงานต่อหลังจาก main หรือฟังก์ชันใด ๆ คืนค่า หากเธรดนั้นยืมตัวแปรภายใน ตัวแปรดังกล่าวอาจถูกทำลายในขณะที่เธรดยังใช้งานอยู่ Rust จึงห้ามกรณีนี้ตั้งแต่เวลาคอมไพล์ และบังคับให้คุณย้ายข้อมูลที่เป็นเจ้าของเข้าไปในคลอเชอร์

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 จะสร้างขอบเขตที่เธรดสามารถยืมตัวแปรภายในได้ ขอบเขตจะรอจนกว่าเธรดทั้งหมดภายในจะทำงานเสร็จ ดังนั้นการอ้างอิงที่ยืมมาจะไม่มีทางกลายเป็นการอ้างอิงที่ไม่ถูกต้อง

คุณสร้างเธรดด้วย s.spawn(...) โดยใช้แฮนเดิลขอบเขต s แทน thread::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);
}

การแบ่งงานข้ามสไลซ์

รูปแบบที่ใช้บ่อยคือแบ่งสไลซ์ แล้วให้แต่ละเธรดประมวลผลส่วนหนึ่ง เธรดแบบมีขอบเขตทำให้รูปแบบนี้สะดวก เพราะแต่ละเธรดสามารถยืมส่วนหนึ่งของสไลซ์ต้นฉบับได้โดยตรง ไม่จำเป็นต้องโคลน

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);
    });
}

การยืมแบบเปลี่ยนแปลงได้ต้องระมัดระวัง

เธรดแบบมีขอบเขตสองเธรดไม่สามารถถือการยืมแบบเปลี่ยนแปลงได้ของข้อมูลเดียวกันพร้อมกัน เพราะจะละเมิดกฎการมีนามแฝงของ Rust หากต้องการแก้ไขข้อมูลร่วมกันจากหลายเธรด คุณยังคงต้องใช้ Mutex แต่เธรดเดียวสามารถยืมส่วนที่ไม่ทับซ้อนกันแบบเปลี่ยนแปลงได้อย่างเป็นเอกสิทธิ์

ด้านล่างนี้ แต่ละเธรดจะแก้ไขครึ่งหนึ่งของอาร์เรย์ที่แยกจากกันผ่าน 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 กับเธรดแบบมีขอบเขตทุกเธรด เมื่อคลอเชอร์ของ 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 คุณไม่ต้องใช้ Arc เพราะขอบเขตช่วยให้เธรดสามารถยืม Mutex ภายในโดยตรงได้อยู่แล้ว

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 หรือการโคลน

ใช้thread::spawnเมื่อเธรดต้องทำงานต่อหลังฟังก์ชันปัจจุบันสิ้นสุด หรือทำงานตลอดอายุของโปรแกรม เธรดแบบมีขอบเขตไม่สามารถออกนอกขอบเขตของตนได้

ตัวอย่างการแมปแบบขนาน

เมื่อนำทุกอย่างมารวมกัน เราจะได้การแมปแบบขนานขนาดเล็กที่แปลงสมาชิกแต่ละตัวของเวกเตอร์ในเธรดของตัวเอง โดยยืมข้อมูลนำเข้า แล้วรวบรวมผลลัพธ์ตามลำดับ

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 ภายในขอบเขต
  • ใช้เธรดแบบมีขอบเขตสำหรับการประมวลผลแบบขนานที่มีขอบเขตและอยู่ภายในฟังก์ชัน

คำถามที่พบบ่อย

บทเรียน “เธรดตามขอบเขต” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “เธรดตามขอบเขต” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Learn Rust Coding ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Learn Rust Coding มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “เธรดตามขอบเขต”

ยืมข้อมูลข้ามเธรด คุณปฏิบัติ Learn Rust Coding ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Learn Rust Coding หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Learn Rust Coding บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “เธรดตามขอบเขต” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Learn Rust Coding นี้ได้ไหม

ได้ บทเรียน Learn Rust Coding ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. แชนเนล mpsc
  2. การแบ่งปันสถานะด้วย Arc/Mutex
  3. เธรดตามขอบเขต
  4. แชนเนล Crossbeam
← กลับไปที่ Learn Rust Coding