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

ดู 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 ได้ return Ready future หลายตัวจะ 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 แทน ซึ่งจะยังไม่คอมไพล์

Filename: src/main.rs
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;
    });
}
Listing 17-23: Await future ใน collection

เราใส่แต่ละ 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

A single-column, three-row table representing a future, fut1, which has data values 0 and 1 in the first two rows and an arrow pointing from the third row back to the second row, representing an internal reference within the future.
Figure 17-4: ประเภทข้อมูล self-referential

ตามค่าเริ่มต้น อย่างไรก็ตาม object ใดที่มี reference ของตัวเองไม่ ปลอดภัยที่จะย้าย เพราะ reference ชี้ไปยัง address memory จริงของอะไร ก็ตามที่พวกมันอ้างถึงเสมอ (ดู Figure 17-5) ถ้าคุณย้ายโครงสร้างข้อมูล เอง reference ภายในเหล่านั้นจะเหลือชี้ไปยังที่เก่า อย่างไรก็ตาม ที่ memory นั้นตอนนี้ไม่ valid สำหรับสิ่งหนึ่ง ค่าของมันจะไม่ถูกอัพเดท เมื่อคุณทำการเปลี่ยนแปลงให้โครงสร้างข้อมูล สำหรับสิ่งอื่น — สำคัญ มากกว่า — คอมพิวเตอร์ตอนนี้ฟรีที่จะใช้ memory นั้นใหม่สำหรับ จุดประสงค์อื่น! คุณลงเอยอ่านข้อมูลไม่เกี่ยวข้องสมบูรณ์ภายหลังได้

Two tables, depicting two futures, fut1 and fut2, each of which has one column and three rows, representing the result of having moved a future out of fut1 into fut2. The first, fut1, is grayed out, with a question mark in each index, representing unknown memory. The second, fut2, has 0 and 1 in the first and second rows and an arrow pointing from its third row back to the second row of fut1, representing a pointer that is referencing the old location in memory of the future before it was moved.
Figure 17-5: ผลที่ไม่ปลอดภัยของการย้ายประเภทข้อมูล self-referential

ในทางทฤษฎี 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 แสดงกระบวนการนี้

Three boxes laid out side by side. The first is labeled “Pin”, the second “b1”, and the third “pinned”. Within “pinned” is a table labeled “fut”, with a single column; it represents a future with cells for each part of the data structure. Its first cell has the value “0”, its second cell has an arrow coming out of it and pointing to the fourth and final cell, which has the value “1” in it, and the third cell has dashed lines and an ellipsis to indicate there may be other parts to the data structure. All together, the “fut” table represents a future which is self-referential. An arrow leaves the box labeled “Pin”, goes through the box labeled “b1” and terminates inside the “pinned” box at the “fut” table.
Figure 17-6: Pin `Box` ที่ชี้ไปยัง type future self-referential

จริง ๆ pointer Box ยังย้ายไปมาอย่างอิสระได้ จำได้ — เราสนใจที่ จะทำให้แน่ใจว่าข้อมูลที่ในที่สุดถูกอ้างถึงอยู่ที่ ถ้า pointer ย้าย ไปมา แต่ข้อมูลที่มันชี้ อยู่ที่เดียวกัน ดังใน Figure 17-7 ไม่มี ปัญหาที่เป็นไปได้ (เป็นการฝึกอิสระ ดู docs สำหรับ type รวมถึงโมดูล std::pin และลองหาว่าคุณทำสิ่งนี้กับ Pin ที่ wrap Box ยังไง) กุญแจคือ type self-referential เองย้ายไม่ได้ เพราะมันยังถูก pin

Four boxes laid out in three rough columns, identical to the previous diagram with a change to the second column. Now there are two boxes in the second column, labeled “b1” and “b2”, “b1” is grayed out, and the arrow from “Pin” goes through “b2” instead of “b1”, indicating that the pointer has moved from “b1” to “b2”, but the data in “pinned” has not moved.
Figure 17-7: ย้าย `Box` ที่ชี้ไปยัง type future self-referential

อย่างไรก็ตาม 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

A box labeled “Pin” on the left with an arrow going from it to a box labeled “String” on the right. The “String” box contains the data 5usize, representing the length of the string, and the letters “h”, “e”, “l”, “l”, and “o” representing the characters of the string “hello” stored in this String instance. A dotted rectangle surrounds the “String” box and its label, but not the “Pin” box.
Figure 17-8: Pin `String` — เส้นประระบุว่า `String` implement trait `Unpin` และดังนั้นไม่ถูก pin

ผลคือ เราทำสิ่งที่จะผิดกฎหมายได้ถ้า String implement !Unpin แทน เช่นแทนที่หนึ่ง string ด้วยอื่นที่ที่เดียวกันแน่นอนใน memory ดังใน Figure 17-9 นี่ไม่ละเมิดสัญญา Pin เพราะ String ไม่มี reference ภายในที่ทำให้มันไม่ปลอดภัยที่จะย้ายไปมา นั่นคือแน่นอน ทำไมมัน implement Unpin ไม่ใช่ !Unpin

The same “hello” string data from the previous example, now labeled “s1” and grayed out. The “Pin” box from the previous example now points to a different String instance, one that is labeled “s2”, is valid, has a length of 7usize, and contains the characters of the string “goodbye”. s2 is surrounded by a dotted rectangle because it, too, implements the Unpin trait.
Figure 17-9: แทนที่ `String` ด้วย `String` ต่างสมบูรณ์ใน memory

ตอนนี้เรารู้พอที่จะเข้าใจ 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;
    });
}
Listing 17-24: Pin future เพื่อเปิดใช้การย้ายพวกมันเข้า vector

ตัวอย่างนี้ตอนนี้คอมไพล์และรัน และเราเพิ่มหรือลบ 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 ที่ implement Future และอนุญาตให้ เราตั้งชื่อ 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 และเธรดทั้งหมด เข้ากันยังไง!