Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ใช้ 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 อื่นจากเธรดใหม่

Filename: src/main.rs
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));
    }
}
Listing 16-1: สร้างเธรดใหม่เพื่อ print สิ่งหนึ่งในขณะที่เธรดหลัก print อย่างอื่น

สังเกตว่าเมื่อเธรดหลักของโปรแกรม 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 ออก

Filename: src/main.rs
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();
}
Listing 16-2: บันทึก 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 แทน แบบนี้:

Filename: src/main.rs
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 อย่างไรก็ตาม นี่จะยังไม่ทำงาน ดังที่ คุณจะเห็นในอีกสักครู่

Filename: src/main.rs
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}
Listing 16-3: การพยายามใช้ vector ที่สร้างโดยเธรดหลักในอีกเธรด

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

Filename: src/main.rs
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();
}
Listing 16-4: เธรดที่มี closure ที่พยายามจับ reference ของ 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 จะคอมไพล์ และรันตามที่เราตั้งใจ

Filename: src/main.rs
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();
}
Listing 16-5: ใช้ keyword 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 ให้มา มาดู สถานการณ์บางอย่างที่เราใช้เธรดได้