ใช้ Thread รันโค้ดพร้อมกัน
ใน OS ปัจจุบันส่วนใหญ่ โค้ดของโปรแกรมที่ execute ถูกรันใน process และ OS จะจัดการหลาย process พร้อมกัน ภายในโปรแกรม คุณยังมีส่วนอิสระ ที่รันพร้อมกันได้ ฟีเจอร์ที่รันส่วนอิสระเหล่านี้เรียก thread ตัวอย่างเช่น web server มีหลายเธรดได้เพื่อให้มันตอบสนองได้มากกว่า หนึ่ง request พร้อมกัน
การแยกการคำนวณในโปรแกรมของคุณเป็นหลายเธรดเพื่อรันหลายงานพร้อมกัน ปรับปรุง performance ได้ แต่ยังเพิ่มความซับซ้อน เพราะเธรดรันพร้อมกัน ได้ ไม่มีการรับประกันโดยกำเนิดเกี่ยวกับลำดับที่ส่วนของโค้ดของคุณบน เธรดต่าง ๆ จะรัน นี่นำไปสู่ปัญหา เช่น:
- Race condition ที่เธรดเข้าถึงข้อมูลหรือ resource ในลำดับไม่ สอดคล้อง
- Deadlock ที่สองเธรดรอกันและกัน ป้องกันทั้งสองเธรดไม่ให้ดำเนินต่อ
- Bug ที่เกิดเฉพาะในสถานการณ์เฉพาะและ reproduce และแก้อย่างเชื่อถือ ได้ยาก
Rust พยายาม mitigate ผลกระทบลบของการใช้เธรด แต่ programming ใน context multithreaded ยังใช้ความคิดอย่างระวังและต้องจัดโครงสร้างโค้ดที่ ต่างจากในโปรแกรมที่รันในเธรดเดียว
ภาษาโปรแกรม implement เธรดในไม่กี่วิธีต่างกัน และ OS หลายตัวให้ API ที่ภาษาโปรแกรมเรียกได้สำหรับสร้างเธรดใหม่ standard library ของ Rust ใช้ model 1:1 ของ implementation เธรด ที่โปรแกรมใช้หนึ่งเธรดของ OS ต่อหนึ่งเธรดภาษา มี crate ที่ implement model อื่นของ threading ที่ทำ trade-off ต่างกับ model 1:1 (ระบบ async ของ Rust ที่เราจะเห็น ในบทถัดไป ให้แนวทาง concurrency อีกแบบด้วย)
สร้าง Thread ใหม่ด้วย spawn
เพื่อสร้างเธรดใหม่ เราเรียกฟังก์ชัน thread::spawn และส่ง closure
(เราพูดถึง closure ในบทที่ 13) ที่บรรจุโค้ดที่เราต้องการรันในเธรด
ใหม่ ตัวอย่างใน Listing 16-1 print text บางตัวจากเธรดหลักและ text
อื่นจากเธรดใหม่
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
สังเกตว่าเมื่อเธรดหลักของโปรแกรม Rust เสร็จ เธรดที่ spawn ทั้งหมด ถูก shut down ไม่ว่าพวกมันจะรันเสร็จหรือไม่ output จากโปรแกรมนี้ อาจต่างกันเล็กน้อยทุกครั้ง แต่มันจะดูคล้ายต่อไปนี้:
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
การเรียก thread::sleep บังคับให้เธรดหยุด execution ของมันเป็นเวลา
สั้น อนุญาตให้เธรดอื่นรัน เธรดอาจสลับเทิร์น แต่นั่นไม่รับประกัน —
มันขึ้นกับว่า OS ของคุณ schedule เธรดยังไง ในการรันนี้ เธรดหลัก
print ก่อน แม้ statement print จากเธรดที่ spawn ปรากฏก่อนในโค้ด
และแม้เราจะบอกเธรดที่ spawn ให้ print จนกว่า i เป็น 9 มันเพียง
ไปถึง 5 ก่อนเธรดหลัก shut down
ถ้าคุณรันโค้ดนี้และเห็นเฉพาะ output จากเธรดหลัก หรือไม่เห็น overlap ใด ลองเพิ่มตัวเลขในช่วงเพื่อสร้างโอกาสมากขึ้นให้ OS สลับระหว่างเธรด
รอให้เธรดทั้งหมดเสร็จ
โค้ดใน Listing 16-1 ไม่เพียงหยุดเธรดที่ spawn ก่อนเวลาส่วนใหญ่ เนื่องจากเธรดหลักจบ แต่เพราะไม่มีการรับประกันลำดับที่เธรดรัน เรา ยังรับประกันไม่ได้ว่าเธรดที่ spawn จะได้รันเลย!
เราแก้ปัญหาของเธรดที่ spawn ไม่รันหรือมันจบก่อนเวลาได้โดยบันทึก
ค่า return ของ thread::spawn ในตัวแปร return type ของ
thread::spawn คือ JoinHandle<T> JoinHandle<T> คือค่า own ที่
เมื่อเราเรียกเมธอด join บนมัน จะรอให้เธรดของมันเสร็จ Listing
16-2 แสดงวิธีใช้ JoinHandle<T> ของเธรดที่เราสร้างใน Listing 16-1
และวิธีเรียก join เพื่อให้แน่ใจว่าเธรดที่ spawn เสร็จก่อน main
ออก
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
JoinHandle<T> จาก thread::spawn เพื่อรับประกันว่าเธรดถูกรันจนเสร็จการเรียก join บน handle block เธรดที่กำลังรันปัจจุบันจนกว่าเธรด
ที่แทนโดย handle จะ terminate Blocking เธรดหมายความว่าเธรดนั้นถูก
ป้องกันจากการทำงานหรือออก เพราะเราใส่การเรียก join หลัง loop for
ของเธรดหลัก การรัน Listing 16-2 ควรสร้าง output คล้ายนี้:
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
เธรดสองตัวยังสลับต่อ แต่เธรดหลักรอเพราะการเรียก handle.join()
และไม่จบจนกว่าเธรดที่ spawn จะเสร็จ
แต่มาดูสิ่งที่เกิดเมื่อเราย้าย handle.join() ไปก่อน loop for
ใน main แทน แบบนี้:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
เธรดหลักจะรอเธรดที่ spawn ให้เสร็จและจากนั้นรัน loop for ของมัน
ดังนั้น output จะไม่ถูก interleave อีก ดังที่แสดงที่นี่:
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
รายละเอียดเล็ก ๆ เช่นที่ที่ join ถูกเรียก กระทบว่าเธรดของคุณรัน
พร้อมกันหรือไม่
ใช้ Closure move กับ Thread
เรามักใช้ keyword move กับ closure ที่ส่งให้ thread::spawn เพราะ
closure จะรับ ownership ของค่าที่มันใช้จาก environment ดังนั้นโอน
ownership ของค่าเหล่านั้นจากเธรดหนึ่งไปยังอีกเธรด ใน
“จับ Reference หรือย้าย Ownership” ในบทที่
13 เราพูดถึง move ใน context ของ closure ตอนนี้เราจะโฟกัสมากขึ้น
ที่การ interaction ระหว่าง move และ thread::spawn
สังเกตใน Listing 16-1 ว่า closure ที่เราส่งให้ thread::spawn ไม่
รับอาร์กิวเมนต์ — เราไม่ใช้ข้อมูลใดจากเธรดหลักในโค้ดของเธรดที่ spawn
เพื่อใช้ข้อมูลจากเธรดหลักในเธรดที่ spawn closure ของเธรดที่ spawn
ต้องจับค่าที่มันต้องการ Listing 16-3 แสดงการพยายามสร้าง vector ใน
เธรดหลักและใช้มันในเธรดที่ spawn อย่างไรก็ตาม นี่จะยังไม่ทำงาน ดังที่
คุณจะเห็นในอีกสักครู่
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
closure ใช้ v ดังนั้นมันจะจับ v และทำให้มันเป็นส่วนของ
environment ของ closure เพราะ thread::spawn รัน closure นี้ใน
เธรดใหม่ เราควรเข้าถึง v ภายในเธรดใหม่นั้นได้ แต่เมื่อเราคอมไพล์
ตัวอย่างนี้ เราได้ error ต่อไปนี้:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {v:?}");
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust infer วิธีจับ v และเพราะ println! ต้องการเพียง reference
ของ v closure พยายาม borrow v อย่างไรก็ตาม มีปัญหา — Rust บอก
ไม่ได้ว่าเธรดที่ spawn จะรันนานเท่าไร ดังนั้นมันไม่รู้ว่า reference
ของ v จะ valid เสมอไหม
Listing 16-4 ให้ scenario ที่มีแนวโน้มมากขึ้นที่จะมี reference ของ
v ที่จะไม่ valid
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
drop(v); // oh no!
handle.join().unwrap();
}
v จากเธรดหลักที่ drop vถ้า Rust อนุญาตให้เรารันโค้ดนี้ มีความเป็นไปได้ที่เธรดที่ spawn
จะถูกใส่ background ทันทีโดยไม่รันเลย เธรดที่ spawn มี reference
ของ v ภายใน แต่เธรดหลัก drop v ทันที โดยใช้ฟังก์ชัน drop
ที่เราพูดถึงในบทที่ 15 จากนั้น เมื่อเธรดที่ spawn เริ่ม execute,
v ไม่ valid อีก ดังนั้น reference ของมันก็ไม่ valid โอ้!
เพื่อแก้ error compiler ใน Listing 16-3 เราใช้คำแนะนำของข้อความ error ได้:
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
โดยเพิ่ม keyword move ก่อน closure เราบังคับให้ closure รับ
ownership ของค่าที่มันใช้แทนการอนุญาตให้ Rust infer ว่ามันควร
borrow ค่า การแก้ Listing 16-3 ที่แสดงใน Listing 16-5 จะคอมไพล์
และรันตามที่เราตั้งใจ
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
move เพื่อบังคับให้ closure รับ ownership ของค่าที่มันใช้เราอาจถูกล่อใจให้ลองสิ่งเดียวกันเพื่อแก้โค้ดใน Listing 16-4 ที่เธรด
หลักเรียก drop โดยใช้ closure move อย่างไรก็ตาม การแก้นี้จะไม่
ทำงานเพราะสิ่งที่ Listing 16-4 พยายามทำไม่ได้รับอนุญาตด้วยเหตุผล
ต่างกัน ถ้าเราเพิ่ม move ให้ closure เราจะย้าย v เข้า
environment ของ closure และเราจะเรียก drop บนมันในเธรดหลักไม่ได้
อีก เราจะได้ error compiler นี้แทน:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move
|
help: consider cloning the value before moving it into the closure
|
6 ~ let value = v.clone();
7 ~ let handle = thread::spawn(move || {
8 ~ println!("Here's a vector: {value:?}");
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error
กฎ ownership ของ Rust ช่วยเราอีก! เราได้ error จากโค้ดใน Listing
16-3 เพราะ Rust เป็น conservative และเพียง borrow v สำหรับเธรด
ซึ่งหมายความว่าเธรดหลักในทางทฤษฎี invalidate reference ของเธรดที่
spawn ได้ โดยบอก Rust ให้ย้าย ownership ของ v ไปยังเธรดที่ spawn
เรากำลังรับประกันต่อ Rust ว่าเธรดหลักจะไม่ใช้ v อีก ถ้าเราเปลี่ยน
Listing 16-4 ในแบบเดียวกัน เราจะละเมิดกฎ ownership เมื่อเราพยายาม
ใช้ v ในเธรดหลัก keyword move override ค่าเริ่มต้น conservative
ของ Rust ในการ borrow — มันไม่ให้เราละเมิดกฎ ownership
ตอนนี้เราครอบคลุมว่าเธรดคืออะไรและเมธอดที่ thread API ให้มา มาดู สถานการณ์บางอย่างที่เราใช้เธรดได้