การประยุกต์ใช้คอนเคอร์เรนซีด้วย Async
ในหัวข้อนี้ เราจะประยุกต์ใช้ async กับความท้าทายด้านคอนเคอร์เรนซีแบบเดียวกับที่เราจัดการด้วยเธรดในบทที่ 16 เนื่องจากเราได้พูดถึงแนวคิดหลักจำนวนมากไปแล้วที่นั่น ในหัวข้อนี้เราจะเน้นไปที่สิ่งที่แตกต่างกันระหว่างเธรดและฟิวเจอร์ส
ในหลายๆ กรณี API สำหรับการทำงานกับคอนเคอร์เรนซีโดยใช้ async จะคล้ายคลึงอย่างมากกับ API สำหรับการใช้เธรด ในกรณีอื่นๆ พวกมันกลับแตกต่างกันค่อนข้างมาก แม้ว่า API จะ ดู คล้ายกันระหว่างเธรดและ async แต่มันมักจะมีพฤติกรรมที่แตกต่างกัน—และเกือบจะเสมอไปที่มีลักษณะด้านประสิทธิภาพที่แตกต่างกัน
การสร้างแทสก์ใหม่ด้วย spawn_task
การทำงานแรกที่เราจัดการในหัวข้อ “การสร้างเธรดใหม่ด้วย spawn” ในบทที่ 16 คือการนับตัวเลขบนสองเธรดที่แยกกัน ลองมาทำสิ่งเดียวกันโดยใช้ async กัน เครต trpl จัดหาฟังก์ชัน spawn_task ซึ่งมีลักษณะคล้ายกับ API thread::spawn อย่างมาก และจัดหาฟังก์ชัน sleep ซึ่งเป็นเวอร์ชัน async ของ API thread::sleep เราสามารถใช้สิ่งเหล่านี้ร่วมกันเพื่อสร้างตัวอย่างการนับเลข ดังแสดงในโค้ดตัวอย่างที่ 17-6
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
}
ในฐานะจุดเริ่มต้น เราตั้งค่าฟังก์ชัน main ด้วย trpl::block_on เพื่อให้ฟังก์ชันระดับบนสุดของเราสามารถเป็น async ได้
หมายเหตุ: ตั้งแต่จุดนี้เป็นต้นไปในบทนี้ ทุกๆ ตัวอย่างจะรวมโค้ดห่อหุ้ม
trpl::block_onในmainแบบเดียวกันนี้เป๊ะๆ ดังนั้นเราจะมักจะข้ามมันไปเช่นเดียวกับที่เราข้ามmainอย่าลืมใส่ลงในโค้ดของคุณด้วย!
จากนั้นเราเขียนสองลูปภายในบล็อกนั้น โดยแต่ละลูปบรรจุการเรียกใช้ trpl::sleep ซึ่งรอเป็นเวลาครึ่งวินาที (500 มิลลิวินาที) ก่อนที่จะส่งข้อความถัดไป เราใส่ลูปหนึ่งไว้ในเนื้อหาของ trpl::spawn_task และอีกลูปหนึ่งไว้ในลูป for ระดับบนสุด เรายังเพิ่ม await ไว้หลังการเรียกใช้ sleep ด้วย
โค้ดนี้มีพฤติกรรมคล้ายกับเวอร์ชันที่ใช้เธรด—รวมถึงความจริงที่ว่าคุณอาจเห็นข้อความปรากฏในลำดับที่แตกต่างกันในเทอร์มินัลของคุณเมื่อรันมัน:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
เวอร์ชันนี้จะหยุดลงทันทีที่ลูป for ในเนื้อหาของบล็อก async หลักทำงานเสร็จสิ้น เนื่องจากแทสก์ที่สร้างขึ้นโดย spawn_task จะถูกปิดเมื่อฟังก์ชัน main จบลง หากคุณต้องการให้มันรันจนกระทั่งแทสก์เสร็จสมบูรณ์ คุณจำเป็นต้องใช้จอยแฮนเดิล (join handle) เพื่อรอให้แทสก์แรกทำงานเสร็จ สำหรับเธรด เราใช้เมธอด join เพื่อ “บล็อก” จนกว่าเธรดจะทำงานเสร็จ ในโค้ดตัวอย่างที่ 17-7 เราสามารถใช้ await เพื่อทำสิ่งเดียวกันได้ เนื่องจากแทสก์แฮนเดิลเองก็เป็นฟิวเจอร์ โดยชนิด Output ของมันคือ Result ดังนั้นเราจึง unwrap มันหลังจาก await แล้วด้วย
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let handle = trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
handle.await.unwrap();
});
}
เวอร์ชันอัปเดตนี้จะรันจนกระทั่ง ทั้งสอง ลูปทำงานเสร็จสิ้น:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
จนถึงตอนนี้ ดูเหมือนว่า async และเธรดจะให้ผลลัพธ์ที่คล้ายกัน เพียงแต่ใช้ไวยากรณ์ต่างกัน: นั่นคือใช้ await แทนที่จะเรียกใช้ join บนจอยแฮนเดิล และรอคอยการเรียกใช้ sleep
ข้อแตกต่างที่ใหญ่กว่าคือเราไม่จำเป็นต้องสร้างเธรดของระบบปฏิบัติการขึ้นมาอีกเธรดเพื่อทำสิ่งนี้ ในความเป็นจริง เราไม่จำเป็นต้องสร้างแทสก์ด้วยซ้ำในที่นี้ เนื่องจากบล็อก async ถูกแปลเป็นฟิวเจอร์สนิรนาม เราจึงสามารถใส่แต่ละลูปไว้ในบล็อก async และให้รันไทม์รันพวกมันทั้งคู่จนเสร็จสมบูรณ์โดยใช้ฟังก์ชัน trpl::join
ในหัวข้อ “การรอให้เธรดทั้งหมดทำงานเสร็จสิ้น” ในบทที่ 16 เราแสดงวิธีใช้เมธอด join บนชนิดข้อมูล JoinHandle ที่คืนค่าเมื่อคุณเรียกใช้ std::thread::spawn ฟังก์ชัน trpl::join ก็คล้ายคลึงกัน แต่ใช้สำหรับฟิวเจอร์ส เมื่อคุณมอบฟิวเจอร์สสองตัวให้มัน มันจะสร้างฟิวเจอร์ใหม่ตัวเดียวขึ้นมาซึ่งมีผลลัพธ์เป็นทูเพิลบรรจุผลลัพธ์ของฟิวเจอร์แต่ละตัวที่คุณส่งเข้าไปเมื่อพวกมันทำงานเสร็จ ทั้งคู่ ดังนั้น ในโค้ดตัวอย่างที่ 17-8 เราใช้ trpl::join เพื่อรอให้ทั้ง fut1 และ fut2 ทำงานเสร็จ เรา ไม่ได้ รอคอย fut1 และ fut2 แต่รอคอยฟิวเจอร์ใหม่ที่สร้างขึ้นโดย trpl::join แทน เราละเลยผลลัพธ์ เพราะมันเป็นเพียงทูเพิลที่บรรจุค่ายูนิตสองค่า
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let fut1 = async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
let fut2 = async {
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
trpl::join(fut1, fut2).await;
});
}
เมื่อเรารันสิ่งนี้ เราจะเห็นฟิวเจอร์สทั้งสองรันจนเสร็จสมบูรณ์:
hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
คราวนี้ คุณจะเห็นลำดับเดียวกันเป๊ะๆ ในทุกๆ ครั้ง ซึ่งแตกต่างอย่างมากจากสิ่งที่เราเห็นกับเธรดและกับ trpl::spawn_task ในโค้ดตัวอย่างที่ 17-7 นั่นเป็นเพราะฟังก์ชัน trpl::join มีความ ยุติธรรม (fair) หมายความว่ามันตรวจสอบฟิวเจอร์แต่ละตัวบ่อยเท่ากัน สลับกันไปมา และไม่ยอมให้ตัวหนึ่งแซงหน้าไปหากอีกตัวหนึ่งพร้อม สำหรับเธรด ระบบปฏิบัติการจะตัดสินใจว่าจะตรวจสอบเธรดใดและปล่อยให้รันนานแค่ไหน ส่วน async ใน Rust รันไทม์จะเป็นผู้ตัดสินใจว่าจะตรวจสอบแทสก์ใด (ในทางปฏิบัติ รายละเอียดจะซับซ้อนขึ้นเนื่องจากรันไทม์ async อาจใช้เธรดระบบปฏิบัติการอยู่เบื้องหลังเป็นส่วนหนึ่งของการจัดการคอนเคอร์เรนซี ดังนั้นการรับประกันความยุติธรรมจึงอาจเป็นงานที่หนักขึ้นสำหรับรันไทม์—แต่มันยังคงเป็นไปได้!) รันไทม์ไม่จำเป็นต้องรับประกันความยุติธรรมสำหรับการทำงานใดๆ และพวกมันมักจัดหา API ที่ต่างกันเพื่อให้คุณเลือกว่าต้องการความยุติธรรมหรือไม่
ลองทดสอบรูปแบบต่างๆ ของการรอคอยฟิวเจอร์สและดูว่าพวกมันทำอะไร:
- นำบล็อก async ออกจากรอบๆ ลูปใดลูปหนึ่งหรือทั้งสองลูป
- รอคอย (await) แต่ละบล็อก async ทันทีหลังจากนิยามมัน
- ห่อหุ้มเฉพาะลูปแรกไว้ในบล็อก async และรอคอยฟิวเจอร์ที่เป็นผลลัพธ์หลังจากเนื้อหาของลูปที่สอง
สำหรับความท้าทายพิเศษ ลองดูว่าคุณสามารถหาสิ่งที่จะเป็นผลลัพธ์ในแต่ละกรณีได้ ก่อน ที่จะรันโค้ดหรือไม่!
การส่งข้อมูลระหว่างสองแทสก์โดยใช้การส่งข้อความ
การแบ่งปันข้อมูลระหว่างฟิวเจอร์สจะเป็นสิ่งที่คุณคุ้นเคยเช่นกัน: เราจะใช้การส่งข้อความ (message passing) อีกครั้ง แต่มันจะเป็นเวอร์ชัน async ของชนิดข้อมูลและฟังก์ชัน เราจะใช้แนวทางที่แตกต่างจากที่เราทำในหัวข้อ “การถ่ายโอนข้อมูลระหว่างเธรดด้วยการส่งข้อความ” ในบทที่ 16 เล็กน้อย เพื่อแสดงข้อแตกต่างหลักบางประการระหว่างคอนเคอร์เรนซีแบบอิงเธรดและแบบอิงฟิวเจอร์ส ในโค้ดตัวอย่างที่ 17-9 เราจะเริ่มด้วยบล็อก async เพียงบล็อกเดียว—โดย ไม่ สร้างแทสก์แยกต่างหากเหมือนกับที่เราเคยสร้างเธรดแยกต่างหาก
extern crate trpl; // required for mdbook test
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let val = String::from("hi");
tx.send(val).unwrap();
let received = rx.recv().await.unwrap();
println!("received '{received}'");
});
}
ในที่นี้ เราใช้ trpl::channel ซึ่งเป็นเวอร์ชัน async ของ API แชนเนลแบบผู้ผลิตหลายราย ผู้บริโภครายเดียวที่เราใช้กับเธรดในบทที่ 16 เวอร์ชัน async ของ API แตกต่างจากเวอร์ชันอิงเธรดเพียงเล็กน้อย: มันใช้ผู้รับ rx แบบเปลี่ยนแปลงได้ (mutable) แทนที่จะเป็นแบบเปลี่ยนแปลงไม่ได้ และเมธอด recv ของมันสร้างฟิวเจอร์ที่เราจำเป็นต้องรอคอย (await) แทนที่จะสร้างค่าโดยตรง คราวนี้เราสามารถส่งข้อความจากผู้ส่งไปยังผู้รับได้ สังเกตว่าเราไม่จำเป็นต้องสร้างเธรดแยกต่างหาก หรือแม้กระทั่งแทสก์แยกต่างหาก เราเพียงต้องรอคอยการเรียกใช้ rx.recv
เมธอดแบบซิงโครนัส Receiver::recv ใน std::mpsc::channel จะบล็อกจนกว่าจะได้รับข้อความ แต่เมธอด trpl::Receiver::recv ไม่ทำเช่นนั้น เพราะมันเป็น async แทนที่จะบล็อก มันจะส่งการควบคุมกลับไปยังรันไทม์จนกว่าจะได้รับข้อความ หรือฝั่งส่งของแชนเนลถูกปิดลง ในทางตรงกันข้าม เราไม่ได้รอคอยการเรียกใช้ send เพราะมันไม่ได้บล็อก มันไม่จำเป็นต้องบล็อก เพราะแชนเนลที่เราส่งลงไปเป็นแบบไม่จำกัดขอบเขต (unbounded)
หมายเหตุ: เนื่องจากโค้ด async ทั้งหมดนี้รันอยู่ในบล็อก async ในการเรียกใช้
trpl::block_onทุกอย่างภายในนั้นจึงสามารถหลีกเลี่ยงการบล็อกได้ อย่างไรก็ตาม โค้ดที่อยู่ ภายนอก มันจะบล็อกตรงการคืนค่าฟังก์ชันblock_onนั่นคือวัตถุประสงค์ทั้งหมดของฟังก์ชันtrpl::block_on: มันช่วยให้คุณ เลือก ได้ว่าจะบล็อกตรงจุดใดในชุดโค้ด async และช่วยสร้างจุดเชื่อมต่อระหว่างโค้ด sync และ async
สังเกตสองสิ่งเกี่ยวกับตัวอย่างนี้ ประการแรก ข้อความจะมาถึงทันที ประการที่สอง แม้ว่าเราจะใช้ฟิวเจอร์ที่นี่ แต่ก็ยังไม่มีคอนเคอร์เรนซี ทุกอย่างในตัวอย่างนี้เกิดขึ้นตามลำดับ เหมือนกับที่มันจะเป็นหากไม่มีฟิวเจอร์สเข้ามาเกี่ยวข้อง
ลองมาจัดการส่วนแรกโดยการส่งชุดข้อความและหยุดพักระหว่างข้อความ ดังแสดงในโค้ดตัวอย่างที่ 17-10
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
}
นอกเหนือจากการส่งข้อความแล้ว เราจำเป็นต้องรับข้อความด้วย ในกรณีนี้ เนื่องจากเรารู้ว่ามีกี่ข้อความส่งเข้ามา เราสามารถทำได้ด้วยตนเองโดยเรียกใช้ rx.recv().await สี่ครั้ง ทว่าในโลกความเป็นจริง เรามักจะรอข้อความที่มีจำนวน ไม่ทราบแน่ชัด ดังนั้นเราจึงจำเป็นต้องรอต่อไปเรื่อยๆ จนกว่าจะพิจารณาได้ว่าไม่มีข้อความเหลืออยู่อีกแล้ว
ในโค้ดตัวอย่างที่ 16-10 เราใช้ลูป for เพื่อประมวลผลไอเทมทั้งหมดที่ได้รับจากแชนเนลแบบซิงโครนัส อย่างไรก็ตาม Rust ยังไม่มีวิธีใช้ลูป for กับชุดไอเทมที่ สร้างขึ้นแบบอะซิงโครนัส ดังนั้นเราจึงจำเป็นต้องใช้ลูปที่เรายังไม่เคยเห็นมาก่อน: นั่นคือลูปแบบมีเงื่อนไข while let สิ่งนี้คือเวอร์ชันลูปของโครงสร้าง if let ที่เราเคยเห็นในหัวข้อ “การควบคุมการทำงานอย่างกระชับด้วย if let และ let...else” ในบทที่ 6 ลูปจะทำงานต่อไปเรื่อยๆ ตราบใดที่รูปแบบที่มันระบุยังคงตรงกับค่า
การเรียกใช้ rx.recv จะสร้างฟิวเจอร์ขึ้นมา ซึ่งเรารอคอย รันไทม์จะหยุดพักฟิวเจอร์จนกว่ามันจะพร้อม เมื่อข้อความมาถึง ฟิวเจอร์จะคลี่คลายค่าออกมาเป็น Some(message) บ่อยเท่าที่มีข้อความมาถึง เมื่อแชนเนลปิดลง ไม่ว่าจะมีข้อความส่งมาถึง เลยหรือไม่ ฟิวเจอร์จะคลี่คลายค่าออกมาเป็น None เพื่อระบุว่าไม่มีค่าใดๆ อีกแล้ว ดังนั้นเราจึงควรหยุดโพลลิง—กล่าวคือ หยุดรอคอย
ลูป while let จะรวบรวมทั้งหมดนี้เข้าด้วยกัน หากผลลัพธ์ของการเรียกใช้ rx.recv().await คือ Some(message) เราจะสามารถเข้าถึงข้อความนั้นและใช้งานมันในเนื้อหาของลูปได้ เช่นเดียวกับที่ทำได้ด้วย if let หากผลลัพธ์เป็น None ลูปจะจบลง ทุกครั้งที่ลูปทำงานครบรอบ มันจะชนกับจุดรออีกครั้ง ดังนั้นรันไทม์จึงหยุดพักมันอีกครั้งจนกว่าข้อความอื่นจะมาถึง
ตอนนี้โค้ดส่งและรับข้อความทั้งหมดได้สำเร็จแล้ว แต่น่าเสียดายที่ยังคงมีปัญหาอยู่อีกสองสามประการ ประการหนึ่งคือ ข้อความไม่ได้มาถึงในช่วงเวลาเว้นห่างกันครึ่งวินาที พวกมันมาถึงพร้อมกันทั้งหมด 2 วินาที (2,000 มิลลิวินาที) หลังจากที่เราเริ่มโปรแกรม อีกประการหนึ่งคือ โปรแกรมนี้ไม่มีวันจบการทำงาน! แต่กลับรอข้อความใหม่ตลอดไป คุณจะต้องปิดมันโดยใช้ ctrl-C
โค้ดภายในบล็อก Async บล็อกเดียวจะทำงานเป็นเส้นตรง
ลองเริ่มจากการตรวจสอบว่าทำไมข้อความจึงมาถึงพร้อมกันทั้งหมดหลังจากการหยุดพักเต็มเวลา แทนที่จะมาถึงพร้อมการหยุดพักระหว่างแต่ละข้อความ ภายในบล็อก async บล็อกหนึ่ง ลำดับที่คีย์เวิร์ด await ปรากฏในโค้ดก็คือลำดับที่พวกมันถูกประมวลผลเมื่อโปรแกรมรัน
มีบล็อก async เพียงบล็อกเดียวในโค้ดตัวอย่างที่ 17-10 ดังนั้นทุกอย่างในนั้นจึงรันเป็นเส้นตรง และยังไม่มีคอนเคอร์เรนซี การเรียกใช้ tx.send ทั้งหมดเกิดขึ้น สลับกับการเรียกใช้ trpl::sleep ทั้งหมดและจุดรอที่เกี่ยวข้อง จากนั้นลูป while let จึงจะได้ผ่านจุด await บนการเรียกใช้ recv
ในการได้พฤติกรรมที่เราต้องการ ซึ่งการหน่วงเวลาการนอนหลับเกิดขึ้นระหว่างแต่ละข้อความ เราจำเป็นต้องแยกการทำงานของ tx และ rx ไว้ในบล็อก async ของตนเอง ดังแสดงในโค้ดตัวอย่างที่ 17-11 จากนั้นรันไทม์สามารถประมวลผลแต่ละอันแยกกันได้โดยใช้ trpl::join เช่นเดียวกับในโค้ดตัวอย่างที่ 17-8 อีกครั้ง เรารอคอยผลลัพธ์ของการเรียกใช้ trpl::join ไม่ใช่ฟิวเจอร์แต่ละตัว หากเรารอคอยฟิวเจอร์แต่ละตัวตามลำดับ เราก็จะกลับไปสู่การทำงานตามลำดับเดิมอีกครั้ง—ซึ่งเป็นสิ่งที่เรารพยายาม ไม่ ทำ
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
ด้วยโค้ดอัปเดตในโค้ดตัวอย่างที่ 17-11 ข้อความจะถูกพิมพ์ที่ช่วงเวลาเว้นห่างกัน 500 มิลลิวินาที แทนที่จะรีบมาพร้อมกันทั้งหมดหลังจาก 2 วินาที
การย้ายความเป็นเจ้าของเข้าไปในบล็อก Async
อย่างไรก็ตาม โปรแกรมนี้ยังคงไม่มีวันจบการทำงาน เนื่องจากวิธีที่ลูป while let โต้ตอบกับ trpl::join:
- ฟิวเจอร์ที่คืนค่าจาก
trpl::joinจะทำงานเสร็จสิ้นก็ต่อเมื่อฟิวเจอร์ส ทั้งสอง ที่ส่งเข้าไปทำงานเสร็จสิ้นแล้ว - ฟิวเจอร์
tx_futจะทำงานเสร็จสิ้นเมื่อมันทำงานนอนหลับเสร็จหลังจากส่งข้อความสุดท้ายในvals - ฟิวเจอร์
rx_futจะไม่เสร็จสิ้นจนกว่าลูปwhile letจะจบลง - ลูป
while letจะไม่จบลงจนกว่าการรอคอยrx.recvจะได้ผลลัพธ์เป็นNone - การรอคอย
rx.recvจะคืนค่าNoneก็ต่อเมื่ออีกฝั่งหนึ่งของแชนเนลถูกปิดลง - แชนเนลจะปิดลงก็ต่อเมื่อเราเรียกใช้
rx.closeหรือเมื่อฝั่งส่งtxถูกคืนค่า (drop) - เราไม่ได้เรียกใช้
rx.closeที่ใดเลย และtxจะไม่ถูกคืนค่าจนกว่าบล็อก async นอกสุดที่ส่งไปยังtrpl::block_onจะจบลง - บล็อกไม่สามารถจบลงได้เพราะมันถูกบล็อกตรงการรัน
trpl::joinจนเสร็จสิ้น ซึ่งทำให้เราวนกลับไปที่ส่วนบนสุดของรายการนี้อีกครั้ง
ในตอนนี้ บล็อก async ที่เราส่งข้อความเพียงแค่ ยืม tx เพราะการส่งข้อความไม่ได้ต้องการความเป็นเจ้าของ แต่ถ้าเราสามารถ ย้าย (move) tx เข้าไปในบล็อก async นั้นได้ มันจะถูกคืนค่าทันทีเมื่อบล็อกนั้นจบลง ในหัวข้อ “การจับการอ้างอิงหรือการย้ายความเป็นเจ้าของ” ในบทที่ 13 คุณได้เรียนรู้วิธีใช้คีย์เวิร์ด move กับโคลเชอร์ และดังที่อภิปรายในหัวข้อ “การใช้โคลเชอร์แบบ move กับเธรด” ในบทที่ 16 เรามักต้องย้ายข้อมูลเข้าไปในโคลเชอร์เมื่อทำงานกับเธรด กลไกพื้นฐานเดียวกันนี้ก็ประยุกต์ใช้กับบล็อก async ด้วย ดังนั้นคีย์เวิร์ด move จึงทำงานกับบล็อก async เช่นเดียวกับที่ทำกับโคลเชอร์
ในโค้ดตัวอย่างที่ 17-12 เราเปลี่ยนบล็อกที่ใช้ส่งข้อความจาก async เป็น async move
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async move {
// --snip--
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
เมื่อเรารันเวอร์ชัน นี้ ของโค้ด มันจะปิดตัวลงอย่างนุ่มนวลหลังจากข้อความสุดท้ายถูกส่งและรับแล้ว ถัดไป ลองมาดูกันว่าต้องเปลี่ยนอะไรบ้างหากต้องการส่งข้อมูลจากฟิวเจอร์มากกว่าหนึ่งตัว
การร่วมฟิวเจอร์สจำนวนมากด้วยมาโคร join!
แชนเนล async นี้ยังเป็นแชนเนลแบบผู้ผลิตหลายรายเช่นกัน ดังนั้นเราจึงสามารถเรียกใช้ clone บน tx ได้หากเราต้องการส่งข้อความจากหลายๆ ฟิวเจอร์ส ดังแสดงในโค้ดตัวอย่างที่ 17-13
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(1500)).await;
}
};
trpl::join!(tx1_fut, tx_fut, rx_fut);
});
}
ขั้นแรก เราโคลน tx สร้างเป็น tx1 ภายนอกบล็อก async แรก เราย้าย tx1 เข้าไปในบล็อกนั้นเหมือนกับที่เราทำก่อนหน้านี้กับ tx จากนั้นในภายหลัง เราย้าย tx ดั้งเดิมเข้าไปในบล็อก async ใหม่ ซึ่งเราส่งข้อความเพิ่มเติมโดยมีการหยุดพักที่ช้าลงเล็กน้อย เราใส่บล็อก async ใหม่นี้ไว้หลังบล็อก async สำหรับรับข้อความ แต่มันสามารถอยู่ก่อนหน้าได้เช่นกัน คีย์สำคัญคือลำดับที่ฟิวเจอร์สถูกรอคอย (await) ไม่ใช่ลำดับที่พวกมันถูกสร้างขึ้น
บล็อก async ทั้งสองสำหรับการส่งข้อความจำเป็นต้องเป็นบล็อก async move เพื่อให้ทั้ง tx และ tx1 ถูกคืนค่าเมื่อบล็อกเหล่านั้นจบลง มิฉะนั้น เราจะจบลงด้วยลูปไม่รู้จบแบบเดิมที่เราเริ่มตั้งต้น
ท้ายที่สุด เราเปลี่ยนจาก trpl::join เป็น trpl::join! เพื่อจัดการกับฟิวเจอร์เพิ่มเติม: มาโคร join! จะรอคอยฟิวเจอร์สจำนวนกี่ตัวก็ได้โดยที่เราทราบจำนวนของฟิวเจอร์สในขั้นตอนคอมไพล์ เราจะอภิปรายเรื่องการรอคอยคอลเลกชันของฟิวเจอร์สที่ไม่ทราบจำนวนในภายหลังของบทนี้
คราวนี้เราจะเห็นข้อความทั้งหมดจากฟิวเจอร์สผู้ส่งทั้งสองตัว และเนื่องจากฟิวเจอร์สผู้ส่งใช้การหยุดพักที่แตกต่างกันเล็กน้อยหลังจากส่ง ข้อความจึงถูกรับในช่วงเวลาเว้นห่างที่แตกต่างกันเหล่านั้นด้วย:
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
เราได้สำรวจวิธีใช้การส่งข้อความเพื่อส่งข้อมูลระหว่างฟิวเจอร์ส วิธีที่โค้ดภายในบล็อก async รันตามลำดับ วิธีการย้ายความเป็นเจ้าของเข้าไปในบล็อก async และวิธีการร่วมฟิวเจอร์สหลายตัว ถัดไป ลองมาอภิปรายถึงวิธีและเหตุผลในการบอกรันไทม์ว่ามันสามารถสลับไปทำแทสก์อื่นได้