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

ใส่ทั้งหมดด้วยกัน — Future, Task และ Thread

ดังที่เราเห็นใน บทที่ 16 เธรดให้แนวทางหนึ่ง ต่อ concurrency เราเห็นอีกแนวทางในบทนี้ — ใช้ async กับ future และ stream ถ้าคุณสงสัยว่าเมื่อจะเลือกหนึ่งวิธีเหนืออื่น คำตอบคือ — ขึ้นกับ! และในหลายกรณี ทางเลือกไม่ใช่เธรด หรือ async แต่เธรด และ async

OS หลายตัวให้ model concurrency ที่ใช้เธรดมาทศวรรษแล้ว และภาษา โปรแกรมหลายภาษาสนับสนุนพวกมัน ผลคือ อย่างไรก็ตาม model เหล่านี้ไม่ ไร้ tradeoff ของพวกมัน บน OS หลายตัว พวกมันใช้ memory พอควรสำหรับ แต่ละเธรด เธรดยังเป็นตัวเลือกเพียงเมื่อ OS และ hardware ของคุณ สนับสนุนพวกมัน ต่างจากคอมพิวเตอร์ desktop และ mobile mainstream บางระบบ embedded ไม่มี OS เลย ดังนั้นพวกมันก็ไม่มีเธรดด้วย

model async ให้ชุด tradeoff ที่ต่างและในที่สุดเสริมกัน ใน model async operation concurrent ไม่ต้องการเธรดของตัวเอง แทน พวกมันรันบน task ได้ เช่นเมื่อเราใช้ trpl::spawn_task เพื่อเริ่มงานจากฟังก์ชัน synchronous ในส่วน stream task คล้ายกับเธรด แต่แทนที่จะถูกจัดการ โดย OS มันถูกจัดการโดยโค้ดระดับ library — runtime

มีเหตุผลที่ API สำหรับ spawn เธรดและ spawn task คล้ายมาก เธรดทำตัว เป็น boundary สำหรับชุดของ operation synchronous — concurrency เป็น ไปได้ ระหว่าง เธรด Task ทำตัวเป็น boundary สำหรับชุดของ operation asynchronous — concurrency เป็นไปได้ทั้ง ระหว่าง และ ภายใน task เพราะ task สลับระหว่าง future ใน body ของมันได้ สุดท้าย future คือหน่วย concurrency ที่ละเอียดที่สุดของ Rust และ แต่ละ future อาจแทนต้นไม้ของ future อื่น runtime — เจาะจง executor ของมัน — จัดการ task และ task จัดการ future ในแง่นั้น task คล้าย กับเธรดที่เบาที่จัดการโดย runtime พร้อมความสามารถเพิ่มที่มาจากการ ถูกจัดการโดย runtime แทน OS

นี่ไม่ได้แปลว่า async task ดีกว่าเธรดเสมอ (หรือในทางกลับกัน) concurrency ด้วยเธรดในบางแง่เป็น model programming ง่ายกว่า concurrency ด้วย async นั่นเป็นจุดแข็งหรือจุดอ่อนได้ เธรดเป็น “fire and forget” หน่อย — พวกมันไม่มีเทียบเท่า native ของ future ดังนั้นพวกมันเพียงรันจนเสร็จโดยไม่ถูก interrupt ยกเว้นโดย OS เอง

และมันกลายเป็นว่าเธรดและ task มักทำงานด้วยกันได้ดีมาก เพราะ task (อย่างน้อยใน runtime บางตัว) ถูกย้ายไปมาระหว่างเธรดได้ จริง ๆ ใต้ ฝ่ามือ runtime ที่เราใช้ — รวมฟังก์ชัน spawn_blocking และ spawn_task — เป็น multithreaded เป็นค่าเริ่มต้น! runtime หลายตัว ใช้แนวทางที่เรียก work stealing เพื่อย้าย task ไปมาระหว่างเธรด อย่างโปร่งใส ตามวิธีที่เธรดถูกใช้ปัจจุบัน เพื่อปรับปรุง performance ของระบบโดยรวม แนวทางนั้นจริง ๆ ต้องการเธรด และ task และดังนั้น future

เมื่อคิดเกี่ยวกับวิธีใดจะใช้เมื่อไร พิจารณากฎ thumb เหล่านี้:

  • ถ้างานเป็น ขนานได้มาก (นั่นคือ CPU-bound) เช่นประมวลผลข้อมูล เยอะที่แต่ละส่วนถูกประมวลผลแยกได้ เธรดเป็นทางเลือกดีกว่า
  • ถ้างานเป็น concurrent มาก (นั่นคือ I/O-bound) เช่นจัดการข้อความ จากแหล่งต่างกันเยอะที่อาจมาในช่วงต่างกันหรืออัตราต่างกัน async เป็นทางเลือกดีกว่า

และถ้าคุณต้องการทั้ง parallelism และ concurrency คุณไม่ต้องเลือก ระหว่างเธรดและ async คุณใช้พวกมันด้วยกันอย่างอิสระ ให้แต่ละเล่นส่วน ที่มันดีที่สุดได้ ตัวอย่างเช่น Listing 17-25 แสดงตัวอย่างค่อนข้าง ทั่วไปของการผสมประเภทนี้ในโค้ด Rust โลกจริง

Filename: src/main.rs
extern crate trpl; // for mdbook test

use std::{thread, time::Duration};

fn main() {
    let (tx, mut rx) = trpl::channel();

    thread::spawn(move || {
        for i in 1..11 {
            tx.send(i).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    trpl::block_on(async {
        while let Some(message) = rx.recv().await {
            println!("{message}");
        }
    });
}
Listing 17-25: ส่งข้อความด้วยโค้ด blocking ในเธรดและ await ข้อความใน block async

เราเริ่มโดยสร้าง channel async แล้ว spawn เธรดที่รับ ownership ของ ฝั่ง sender ของ channel โดยใช้ keyword move ภายในเธรด เราส่ง ตัวเลข 1 ถึง 10, sleep หนึ่งวินาทีระหว่างแต่ละตัว สุดท้าย เรารัน future ที่สร้างกับ block async ที่ส่งให้ trpl::block_on เพียง เหมือนที่เราทำตลอดบท ใน future นั้น เรา await ข้อความเหล่านั้น เพียงเหมือนใน ตัวอย่าง message-passing อื่นที่เราเห็น

เพื่อกลับไปยัง scenario ที่เราเปิดบทด้วย จินตนาการการรันชุดของ task encoding video โดยใช้เธรดเฉพาะ (เพราะ encoding video เป็น compute-bound) แต่แจ้ง UI ว่า operation เหล่านั้นเสร็จด้วย channel async มีตัวอย่างนับไม่ถ้วนของการรวมประเภทเหล่านี้ใน use case โลก จริง

สรุป

นี่ไม่ใช่ครั้งสุดท้ายที่คุณจะเห็น concurrency ในหนังสือเล่มนี้ โปรเจกต์ใน บทที่ 21 จะใช้แนวคิดเหล่านี้ใน สถานการณ์ที่ realistic มากกว่าตัวอย่างที่ง่ายที่พูดที่นี่ และ เปรียบเทียบการแก้ปัญหากับ threading เทียบกับ task และ future โดยตรงกว่า

ไม่ว่าคุณเลือกแนวทางใดเหล่านี้ Rust ให้คุณเครื่องมือที่คุณต้องการ เพื่อเขียนโค้ด concurrent ปลอดภัยและเร็ว — ไม่ว่าสำหรับ web server high-throughput หรือ OS embedded

ถัดไป เราจะพูดถึงวิธี idiomatic ในการ model ปัญหาและจัดโครงสร้าง วิธีแก้เมื่อโปรแกรม Rust ของคุณใหญ่ขึ้น นอกจากนี้ เราจะพูดถึงว่า idiom ของ Rust เกี่ยวกับที่คุณอาจคุ้นเคยจาก object-oriented programming อย่างไร