ดู Trait สำหรับ Async ใกล้ขึ้น
ตลอดบท เราใช้ trait Future, Stream และ StreamExt ในวิธีต่าง ๆ
จนถึงตอนนี้ อย่างไรก็ตาม เราหลีกเลี่ยงการเข้าไกลเกินไปในรายละเอียด
ว่าพวกมันทำงานยังไงหรือพวกมันเข้ากันยังไง ซึ่งโอเคส่วนใหญ่ของเวลา
สำหรับงาน Rust ประจำวันของคุณ บางครั้ง อย่างไรก็ตาม คุณจะพบ
สถานการณ์ที่คุณต้องเข้าใจรายละเอียดของ trait เหล่านี้มากกว่า รวมถึง
type Pin และ trait Unpin ในส่วนนี้ เราจะขุดเข้าไปเพียงพอที่จะ
ช่วยใน scenario เหล่านั้น ยังเก็บการดำดิ่งลึก จริง ๆ ไว้สำหรับ
documentation อื่น
Trait Future
มาเริ่มโดยดู trait Future ทำงานยังไงใกล้ขึ้น นี่คือวิธีที่ Rust
นิยามมัน:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
นิยาม trait นั้นรวม type ใหม่และ syntax บางอย่างที่เรายังไม่เห็น ก่อน ดังนั้นมาเดินผ่านนิยามทีละชิ้น
ก่อนอื่น associated type Output ของ Future บอกว่า future
resolve เป็นอะไร นี่เปรียบเทียบกับ associated type Item สำหรับ
trait Iterator ที่สอง Future มีเมธอด poll ซึ่งรับ reference
พิเศษ Pin สำหรับ parameter self ของมันและ mutable reference
ของ type Context และ return Poll<Self::Output> เราจะพูดเพิ่ม
เกี่ยวกับ Pin และ Context ในอีกครู่ ตอนนี้ มาโฟกัสที่สิ่งที่
เมธอด return — type Poll:
#![allow(unused)]
fn main() {
pub enum Poll<T> {
Ready(T),
Pending,
}
}
type Poll นี้คล้ายกับ Option มันมีหนึ่ง variant ที่มีค่า
Ready(T) และอันหนึ่งที่ไม่มี Pending Poll หมายความค่อนข้าง
ต่างจาก Option อย่างไรก็ตาม! variant Pending ระบุว่า future
ยังมีงานที่จะทำ ดังนั้น caller จะต้องตรวจสอบอีกครั้งภายหลัง variant
Ready ระบุว่า Future เสร็จงานของมันแล้วและค่า T ใช้ได้
สังเกต — หายากที่จะต้องเรียก
pollโดยตรง แต่ถ้าคุณต้อง เก็บในใจ ว่ากับ future ส่วนใหญ่ caller ไม่ควรเรียกpollอีกหลัง future ได้ returnReadyfuture หลายตัวจะ panic ถ้าถูก poll อีกหลัง กลายเป็น ready future ที่ปลอดภัยที่จะ poll อีกจะพูดเช่นนั้น ชัดเจนใน documentation ของพวกมัน นี่คล้ายกับวิธีที่Iterator::nextทำตัว
เมื่อคุณเห็นโค้ดที่ใช้ await Rust คอมไพล์มันใต้ฝ่ามือเป็นโค้ดที่
เรียก poll ถ้าคุณมองกลับที่ Listing 17-4 ที่เรา print title หน้า
สำหรับ URL เดียวเมื่อมัน resolve Rust คอมไพล์มันเป็นอะไรประเภท
(แม้ไม่แน่นอน) แบบนี้:
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}
เราควรทำอะไรเมื่อ future ยัง Pending? เราต้องการวิธีบางอย่างที่
จะลองอีก และอีก และอีก จนกว่า future สุดท้ายพร้อม อีกแง่หนึ่ง เรา
ต้องการ loop:
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}
ถ้า Rust คอมไพล์มันเป็นโค้ดนั้นแน่นอน อย่างไรก็ตาม ทุก await จะ
เป็น blocking — แน่นอนตรงข้ามกับสิ่งที่เรากำลังไป! แทน Rust รับ
ประกันว่า loop ส่งการควบคุมไปยังอะไรที่ pause งานบน future นี้เพื่อ
ทำงานบน future อื่นแล้วตรวจสอบอันนี้อีกภายหลังได้ ดังที่เราเห็น
สิ่งนั้นคือ async runtime และงานการ scheduling และการประสานนี้คือ
หนึ่งในงานหลักของมัน
ในส่วน
“ส่งข้อมูลระหว่างสอง Task โดยใช้ Message Passing”
เราอธิบายการรอบน rx.recv การเรียก recv return future และการ
await future poll มัน เราระบุว่า runtime จะ pause future จนกว่ามัน
พร้อมด้วย Some(message) หรือ None เมื่อ channel ปิด ด้วยความ
เข้าใจลึกขึ้นของเราเกี่ยวกับ trait Future และเจาะจง
Future::poll เราเห็นว่ามันทำงานยังไง runtime รู้ว่า future ไม่
พร้อมเมื่อมัน return Poll::Pending ในทางกลับกัน runtime รู้ว่า
future พร้อม และก้าวหน้ามันเมื่อ poll return
Poll::Ready(Some(message)) หรือ Poll::Ready(None)
รายละเอียดแน่นอนของวิธีที่ runtime ทำสิ่งนั้นอยู่นอก scope ของ หนังสือเล่มนี้ แต่กุญแจคือเห็นกลไกพื้นฐานของ future — runtime poll แต่ละ future ที่มันรับผิดชอบ ใส่ future กลับ sleep เมื่อมันยังไม่ พร้อม
Type Pin และ Trait Unpin
กลับใน Listing 17-13 เราใช้มาโคร trpl::join! เพื่อ await สาม
future อย่างไรก็ตาม ทั่วไปที่จะมี collection เช่น vector ที่บรรจุ
จำนวน future ที่จะไม่รู้จนถึง runtime มาเปลี่ยน Listing 17-13 เป็น
โค้ดใน Listing 17-23 ที่ใส่สาม future เข้า vector และเรียกฟังก์ชัน
trpl::join_all แทน ซึ่งจะยังไม่คอมไพล์
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_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
// --snip--
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_secs(1)).await;
}
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
เราใส่แต่ละ future ภายใน Box เพื่อทำให้พวกมันเป็น trait object
เพียงเหมือนที่เราทำในส่วน “Return Error จาก run” ในบทที่ 12 (เรา
จะครอบคลุม trait object ในรายละเอียดในบทที่ 18) การใช้ trait
object ให้เราปฏิบัติกับแต่ละ future anonymous ที่ผลิตโดย type
เหล่านี้เป็น type เดียวกัน เพราะพวกมันทั้งหมด implement trait Future
นี่อาจน่าประหลาดใจ ในที่สุด ไม่มี block async ใด return อะไร ดังนั้น
แต่ละอันผลิต Future<Output = ()> จำได้ว่า Future เป็น trait
อย่างไรก็ตาม และ compiler สร้าง enum unique สำหรับแต่ละ block async
แม้เมื่อพวกมันมี output type เหมือนกัน เพียงเหมือนคุณใส่ struct ที่
เขียนด้วยมือต่างกันสองตัวใน Vec ไม่ได้ คุณผสม enum ที่ compiler
สร้างไม่ได้
จากนั้นเราส่ง collection ของ future ให้ฟังก์ชัน trpl::join_all และ
await ผล อย่างไรก็ตาม นี่ไม่คอมไพล์ นี่คือส่วนที่เกี่ยวข้องของ
ข้อความ error
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
note ในข้อความ error นี้บอกเราว่าเราควรใช้มาโคร pin! เพื่อ pin
ค่า ซึ่งหมายความว่าใส่พวกมันภายใน type Pin ที่รับประกันว่าค่าจะ
ไม่ถูกย้ายใน memory ข้อความ error บอกว่า pinning ต้องการเพราะ
dyn Future<Output = ()> ต้อง implement trait Unpin และมัน
ปัจจุบันไม่
ฟังก์ชัน trpl::join_all return struct ที่เรียก JoinAll struct
นั้นเป็น generic เหนือ type F ซึ่งถูกจำกัดที่จะ implement trait
Future การ await future โดยตรงด้วย await pin future โดยปริยาย
นั่นคือเหตุผลที่เราไม่ต้องใช้ pin! ทุกที่ที่เราต้องการ await
future
อย่างไรก็ตาม เราไม่ได้ await future โดยตรงที่นี่ แทน เราสร้าง future
ใหม่ JoinAll โดยส่ง collection ของ future ให้ฟังก์ชัน join_all
signature สำหรับ join_all ต้องการให้ type ของ item ใน collection
ทั้งหมด implement trait Future และ Box<T> implement Future
เฉพาะถ้า T ที่มัน wrap คือ future ที่ implement trait Unpin
นั่นมาก! เพื่อเข้าใจจริง ๆ มาดำดิ่งเพิ่มเข้าไปในวิธีที่ trait
Future ทำงานจริง ๆ โดยเฉพาะรอบ pinning ดูนิยามของ trait Future
อีก:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
parameter cx และ type Context ของมันเป็นกุญแจที่ runtime รู้
จริง ๆ เมื่อจะตรวจสอบ future ใดในขณะที่ยังเป็น lazy อีกครั้ง
รายละเอียดของวิธีที่ทำงานอยู่นอก scope ของบทนี้ และคุณโดยทั่วไป
เพียงต้องคิดเกี่ยวกับนี้เมื่อเขียน implementation Future กำหนด
เอง เราจะโฟกัสแทนที่ type สำหรับ self เพราะนี่เป็นครั้งแรกที่เรา
เห็นเมธอดที่ self มี type annotation type annotation สำหรับ
self ทำงานเหมือน type annotation สำหรับ parameter ฟังก์ชันอื่น
แต่ด้วยสองความแตกต่างหลัก:
- มันบอก Rust ว่า type
selfต้องเป็นอะไรเพื่อให้เมธอดถูกเรียกได้ - มันเป็นเพียง type ใดไม่ได้ มันถูกจำกัดที่ type ที่เมธอดถูก
implement, reference หรือ smart pointer ของ type นั้น หรือ
Pinที่ wrap reference ของ type นั้น
เราจะเห็นเพิ่มเกี่ยวกับ syntax นี้ใน บทที่ 18
ตอนนี้ เพียงพอที่จะรู้ว่าถ้าเราต้องการ poll future เพื่อตรวจสอบว่า
มัน Pending หรือ Ready(Output) เราต้องการ mutable reference
ที่ wrap ด้วย Pin ของ type
Pin คือ wrapper สำหรับ type คล้าย pointer เช่น &, &mut, Box
และ Rc (เชิงเทคนิค Pin ทำงานกับ type ที่ implement trait
Deref หรือ DerefMut แต่นี่เทียบเท่าจริงกับการทำงานเพียงกับ
reference และ smart pointer) Pin ไม่ใช่ pointer เองและไม่มี
พฤติกรรมของตัวเองเหมือน Rc และ Arc ทำกับ reference counting —
มันเป็นเพียงเครื่องมือที่ compiler ใช้เพื่อบังคับข้อจำกัดบนการใช้
pointer
จำได้ว่า await ถูก implement ในแง่ของการเรียก poll เริ่มอธิบาย
ข้อความ error ที่เราเห็นก่อนหน้า แต่นั่นอยู่ในแง่ของ Unpin ไม่ใช่
Pin แล้ว Pin เกี่ยวกับ Unpin ยังไง และทำไม Future ต้องการ
self ที่จะอยู่ใน type Pin เพื่อเรียก poll?
จำได้จากต้นบทว่าชุด await point ใน future ถูกคอมไพล์เป็น state machine และ compiler ทำให้แน่ใจว่า state machine นั้นตามกฎปกติของ Rust ทั้งหมดรอบความปลอดภัย รวมถึง borrowing และ ownership เพื่อทำ สิ่งนั้นทำงาน Rust ดูว่าข้อมูลใดต้องการระหว่าง await point หนึ่ง และ await point ถัดไปหรือสิ้นสุดของ block async มันจากนั้นสร้าง variant ที่ตรงกันใน state machine ที่คอมไพล์ แต่ละ variant ได้ สิทธิ์เข้าถึงที่มันต้องการของข้อมูลที่จะถูกใช้ในส่วนนั้นของ source code ไม่ว่าโดยรับ ownership ของข้อมูลนั้นหรือรับ mutable หรือ immutable reference ของมัน
จนถึงตอนนี้ ดี — ถ้าเราได้ผิดอะไรเกี่ยวกับ ownership หรือ reference
ใน block async ที่ให้ borrow checker จะบอกเรา เมื่อเราต้องการย้าย
future ที่ตรงกับ block นั้น — เช่นย้ายมันเข้า Vec เพื่อส่งให้
join_all — สิ่งต่าง ๆ ยุ่งยากขึ้น
เมื่อเราย้าย future — ไม่ว่าโดย push มันเข้าโครงสร้างข้อมูลเพื่อใช้
เป็น iterator กับ join_all หรือโดย return มันจากฟังก์ชัน — นั่น
หมายความจริง ๆ ว่าย้าย state machine ที่ Rust สร้างให้เรา และ
ต่างจาก type อื่นส่วนใหญ่ใน Rust future ที่ Rust สร้างสำหรับ block
async ลงเอยด้วย reference ของตัวมันเองใน field ของ variant ใดที่
ให้ ดังที่แสดงในภาพประกอบที่ง่ายใน Figure 17-4
ตามค่าเริ่มต้น อย่างไรก็ตาม object ใดที่มี reference ของตัวเองไม่ ปลอดภัยที่จะย้าย เพราะ reference ชี้ไปยัง address memory จริงของอะไร ก็ตามที่พวกมันอ้างถึงเสมอ (ดู Figure 17-5) ถ้าคุณย้ายโครงสร้างข้อมูล เอง reference ภายในเหล่านั้นจะเหลือชี้ไปยังที่เก่า อย่างไรก็ตาม ที่ memory นั้นตอนนี้ไม่ valid สำหรับสิ่งหนึ่ง ค่าของมันจะไม่ถูกอัพเดท เมื่อคุณทำการเปลี่ยนแปลงให้โครงสร้างข้อมูล สำหรับสิ่งอื่น — สำคัญ มากกว่า — คอมพิวเตอร์ตอนนี้ฟรีที่จะใช้ memory นั้นใหม่สำหรับ จุดประสงค์อื่น! คุณลงเอยอ่านข้อมูลไม่เกี่ยวข้องสมบูรณ์ภายหลังได้
ในทางทฤษฎี Rust compiler ลองอัพเดททุก reference ของ object เมื่อใด ก็ตามที่มันถูกย้ายได้ แต่นั่นเพิ่ม overhead performance มากได้ โดย เฉพาะถ้า web ของ reference ทั้งหมดต้องการการอัพเดท ถ้าเราทำให้ แน่ใจได้แทนว่าโครงสร้างข้อมูลในคำถาม_ไม่ย้ายใน memory_ เราจะไม่ต้อง อัพเดท reference ใด นี่คือแน่นอนสิ่งที่ borrow checker ของ Rust มีไว้สำหรับ — ในโค้ดปลอดภัย มันป้องกันคุณจากการย้าย item ใดที่มี active reference ของมัน
Pin build บนนั้นเพื่อให้เราการรับประกันแน่นอนที่เราต้องการ เมื่อ
เรา pin ค่าโดย wrap pointer ของค่านั้นใน Pin มันย้ายไม่ได้อีก
ดังนั้น ถ้าคุณมี Pin<Box<SomeType>> คุณจริง ๆ pin ค่า SomeType
ไม่ใช่ pointer Box Figure 17-6 แสดงกระบวนการนี้
จริง ๆ pointer Box ยังย้ายไปมาอย่างอิสระได้ จำได้ — เราสนใจที่
จะทำให้แน่ใจว่าข้อมูลที่ในที่สุดถูกอ้างถึงอยู่ที่ ถ้า pointer ย้าย
ไปมา แต่ข้อมูลที่มันชี้ อยู่ที่เดียวกัน ดังใน Figure 17-7 ไม่มี
ปัญหาที่เป็นไปได้ (เป็นการฝึกอิสระ ดู docs สำหรับ type รวมถึงโมดูล
std::pin และลองหาว่าคุณทำสิ่งนี้กับ Pin ที่ wrap Box ยังไง)
กุญแจคือ type self-referential เองย้ายไม่ได้ เพราะมันยังถูก pin
อย่างไรก็ตาม type ส่วนใหญ่ปลอดภัยสมบูรณ์ที่จะย้ายไปมา แม้พวกมัน
บังเอิญจะอยู่หลัง pointer Pin เราต้องคิดเกี่ยวกับ pinning เพียง
เมื่อ item มี reference ภายใน ค่า primitive เช่นตัวเลขและ Boolean
ปลอดภัยเพราะพวกมันชัดเจนไม่มี reference ภายใน type ส่วนใหญ่ที่คุณ
ทำงานปกติใน Rust ก็ไม่ คุณย้าย Vec ไปมาได้ ตัวอย่างเช่น โดยไม่
กังวล จากสิ่งที่เราเห็นจนถึงตอนนี้ ถ้าคุณมี Pin<Vec<String>>
คุณจะต้องทำทุกอย่างผ่าน API ปลอดภัยแต่จำกัดที่ Pin ให้ แม้
Vec<String> ปลอดภัยที่จะย้ายเสมอถ้าไม่มี reference อื่นของมัน
เราต้องการวิธีในการบอก compiler ว่ามันโอเคที่จะย้าย item ไปมาใน
กรณีเช่นนี้ — และนั่นคือที่ที่ Unpin เข้ามาเล่น
Unpin คือ marker trait คล้ายกับ trait Send และ Sync ที่เรา
เห็นในบทที่ 16 และดังนั้นไม่มี functionality ของตัวเอง Marker
trait มีอยู่เพียงเพื่อบอก compiler ว่ามันปลอดภัยที่จะใช้ type ที่
implement trait ที่ให้ใน context เฉพาะ Unpin แจ้ง compiler ว่า
type ที่ให้ ไม่ ต้องรักษาการรับประกันใดเกี่ยวกับว่าค่าในคำถามถูก
ย้ายปลอดภัยได้
เพียงเหมือนกับ Send และ Sync compiler implement Unpin
อัตโนมัติสำหรับทุก type ที่มันพิสูจน์ได้ว่าปลอดภัย กรณีพิเศษ คล้าย
กับ Send และ Sync อีก คือที่ Unpin ไม่ ถูก implement
สำหรับ type notation สำหรับนี้คือ
impl !Unpin for SomeType ที่
SomeType คือชื่อของ type ที่ ต้อง รักษา
การรับประกันเหล่านั้นเพื่อปลอดภัยเมื่อใดก็ตามที่ pointer ของ type
นั้นถูกใช้ใน Pin
อีกแง่หนึ่ง มีสองสิ่งที่จะเก็บในใจเกี่ยวกับความสัมพันธ์ระหว่าง
Pin และ Unpin ก่อนอื่น Unpin คือกรณี “ปกติ” และ !Unpin คือ
กรณีพิเศษ ที่สอง ว่า type implement Unpin หรือ !Unpin เพียง
สำคัญเมื่อคุณกำลังใช้ pinned pointer ของ type นั้นเช่น
Pin<&mut SomeType>
เพื่อทำให้นั้นเป็นรูปธรรม คิดเกี่ยวกับ String — มันมีความยาวและ
character Unicode ที่ประกอบมัน เรา wrap String ใน Pin ได้ ดังที่
เห็นใน Figure 17-8 อย่างไรก็ตาม String implement Unpin
อัตโนมัติ เหมือนกับ type อื่นส่วนใหญ่ใน Rust
ผลคือ เราทำสิ่งที่จะผิดกฎหมายได้ถ้า String implement !Unpin
แทน เช่นแทนที่หนึ่ง string ด้วยอื่นที่ที่เดียวกันแน่นอนใน memory
ดังใน Figure 17-9 นี่ไม่ละเมิดสัญญา Pin เพราะ String ไม่มี
reference ภายในที่ทำให้มันไม่ปลอดภัยที่จะย้ายไปมา นั่นคือแน่นอน
ทำไมมัน implement Unpin ไม่ใช่ !Unpin
ตอนนี้เรารู้พอที่จะเข้าใจ error ที่รายงานสำหรับการเรียก join_all
นั้นกลับใน Listing 17-23 เราดั้งเดิมพยายามย้าย future ที่ผลิตโดย
block async เข้า Vec<Box<dyn Future<Output = ()>>> แต่ดังที่เรา
เห็น future เหล่านั้นอาจมี reference ภายใน ดังนั้นพวกมันไม่
implement Unpin อัตโนมัติ เมื่อเรา pin พวกมัน เราส่ง type Pin
ที่ได้เข้า Vec ได้ มั่นใจว่าข้อมูล underlying ใน future จะ ไม่
ถูกย้าย Listing 17-24 แสดงวิธีแก้โค้ดโดยเรียกมาโคร pin! ที่แต่
ละสาม future ถูกนิยามและปรับ type trait object
extern crate trpl; // required for mdbook test
use std::pin::{Pin, pin};
// --snip--
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = pin!(async move {
// --snip--
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_secs(1)).await;
}
});
let rx_fut = pin!(async {
// --snip--
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
let tx_fut = pin!(async move {
// --snip--
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_secs(1)).await;
}
});
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
});
}
ตัวอย่างนี้ตอนนี้คอมไพล์และรัน และเราเพิ่มหรือลบ future จาก vector ที่ runtime และ join พวกมันทั้งหมดได้
Pin และ Unpin สำคัญส่วนใหญ่สำหรับการ build library ระดับต่ำ
หรือเมื่อคุณกำลัง build runtime เอง ไม่ใช่สำหรับโค้ด Rust ประจำวัน
เมื่อคุณเห็น trait เหล่านี้ในข้อความ error อย่างไรก็ตาม ตอนนี้คุณ
จะมีไอเดียดีขึ้นในการแก้โค้ดของคุณ!
สังเกต — การรวมของ
PinและUnpinนี้ทำให้เป็นไปได้ที่จะ implement ทั้ง class ของ type ซับซ้อนใน Rust ปลอดภัยที่มิฉะนั้น จะพิสูจน์ท้าทายเพราะพวกมันเป็น self-referential type ที่ต้องการPinปรากฏที่ทั่วไปที่สุดใน async Rust วันนี้ แต่เป็นบางครั้ง คุณอาจเห็นพวกมันใน context อื่นด้วยเจาะจงว่า
PinและUnpinทำงานยังไง และกฎที่พวกมันต้องรักษา ถูกครอบคลุมอย่างกว้างขวางใน API documentation สำหรับstd::pinดังนั้นถ้าคุณสนใจในการเรียนเพิ่ม นั่นเป็นที่ดีที่จะเริ่มถ้าคุณต้องการเข้าใจว่าสิ่งต่าง ๆ ทำงานยังไงใต้ฝ่ามือในรายละเอียด มากกว่า ดูบท 2 และ 4 ของ Asynchronous Programming in Rust
Trait Stream
ตอนนี้คุณมีความเข้าใจลึกขึ้นบน trait Future, Pin และ Unpin
เราหันความสนใจของเราไปยัง trait Stream ได้ ดังที่คุณเรียนก่อน
หน้านี้ในบท stream คล้ายกับ asynchronous iterator ต่างจาก Iterator
และ Future อย่างไรก็ตาม Stream ไม่มีนิยามใน standard library
ณ เวลาที่เขียนนี้ แต่ มี นิยามทั่วไปมากจาก crate futures ที่ใช้
ตลอด ecosystem
มาทบทวนนิยามของ trait Iterator และ Future ก่อนดูว่า trait
Stream อาจรวมพวกมันด้วยกันยังไง จาก Iterator เรามีไอเดียของ
ลำดับ — เมธอด next ของมันให้ Option<Self::Item> จาก Future
เรามีไอเดียของความพร้อมเหนือเวลา — เมธอด poll ของมันให้
Poll<Self::Output> เพื่อแทนลำดับของ item ที่กลายเป็นพร้อมเหนือ
เวลา เรานิยาม trait Stream ที่ใส่ฟีเจอร์เหล่านั้นด้วยกัน:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
}
trait Stream นิยาม associated type ที่เรียก Item สำหรับ type
ของ item ที่ผลิตโดย stream นี่คล้ายกับ Iterator ที่อาจมี item
ศูนย์ถึงหลาย และต่างจาก Future ที่มี Output เดียวเสมอ แม้มัน
เป็น unit type ()
Stream ยังนิยามเมธอดเพื่อรับ item เหล่านั้น เราเรียกมัน poll_next
เพื่อทำให้ชัดเจนว่ามัน poll ในแบบเดียวกับที่ Future::poll ทำและ
ผลิตลำดับของ item ในแบบเดียวกับที่ Iterator::next ทำ return type
ของมันรวม Poll กับ Option type ภายนอกคือ Poll เพราะมันต้อง
ถูกตรวจสอบความพร้อม เพียงเหมือน future ทำ type ภายในคือ Option
เพราะมันต้องส่งสัญญาณว่ามีข้อความเพิ่มไหม เพียงเหมือน iterator ทำ
อะไรคล้ายมากกับนิยามนี้น่าจะลงเอยเป็นส่วนของ standard library ของ Rust ในระหว่างนี้ มันเป็นส่วนของชุดเครื่องมือของ runtime ส่วนใหญ่ ดังนั้นคุณพึ่งมันได้ และทุกอย่างที่เราครอบคลุมต่อไปควรใช้โดยทั่วไป!
ในตัวอย่างที่เราเห็นในส่วน
“Stream — Future ในลำดับ” อย่างไรก็ตาม
เราไม่ใช้ poll_next หรือ Stream แต่แทนใช้ next และ
StreamExt เรา ทำงานโดยตรง ในแง่ของ API poll_next โดยเขียน
state machine Stream ของเราเองด้วยมือได้ แน่นอน เพียงเหมือนที่เรา
ทำงานกับ future โดยตรง ผ่านเมธอด poll ของพวกมันได้ การใช้
await ดีกว่ามาก อย่างไรก็ตาม และ trait StreamExt ให้เมธอด
next เพื่อเราทำสิ่งนั้นได้:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>>;
}
trait StreamExt: Stream {
async fn next(&mut self) -> Option<Self::Item>
where
Self: Unpin;
// other methods...
}
}
สังเกต — นิยามจริงที่เราใช้ก่อนหน้านี้ในบทดูต่างเล็กน้อยจากนี้ เพราะมันสนับสนุน version ของ Rust ที่ยังไม่สนับสนุนการใช้ฟังก์ชัน async ใน trait ผลคือ มันดูแบบนี้:
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;type
Nextนั้นคือstructที่ implementFutureและอนุญาตให้ เราตั้งชื่อ lifetime ของ reference ของselfด้วยNext<'_, Self>เพื่อให้awaitทำงานกับเมธอดนี้ได้
trait StreamExt ยังเป็นบ้านของเมธอดที่น่าสนใจทั้งหมดที่ใช้กับ
stream ได้ StreamExt ถูก implement อัตโนมัติสำหรับทุก type ที่
implement Stream แต่ trait เหล่านี้ถูกนิยามแยกเพื่อเปิดใช้
community ในการ iterate บน API ความสะดวกโดยไม่กระทบ trait พื้นฐาน
ใน version ของ StreamExt ที่ใช้ใน crate trpl trait ไม่เพียง
นิยามเมธอด next แต่ยังให้ implementation เริ่มต้นของ next ที่
จัดการรายละเอียดของการเรียก Stream::poll_next ถูกต้อง นี่หมายความ
ว่าแม้เมื่อคุณต้องเขียนประเภทข้อมูล streaming ของตัวเอง คุณ
เพียง ต้อง implement Stream และจากนั้นใครก็ตามที่ใช้ประเภท
ข้อมูลของคุณใช้ StreamExt และเมธอดของมันกับมันได้อัตโนมัติ
นั่นคือทั้งหมดที่เราจะครอบคลุมสำหรับรายละเอียดระดับต่ำบน trait เหล่า นี้ เพื่อปิด มาพิจารณาว่า future (รวม stream), task และเธรดทั้งหมด เข้ากันยังไง!