เธรดตามขอบเขต
ยืมข้อมูลข้ามเธรด
เธรดตามขอบเขต เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ