Future และ Syntax ของ Async
element หลักของ asynchronous programming ใน Rust คือ future และ
keyword async และ await ของ Rust
future คือค่าที่อาจไม่พร้อมตอนนี้แต่จะพร้อมในจุดในอนาคต (แนวคิด
เดียวกันนี้ปรากฏในหลายภาษา บางครั้งภายใต้ชื่ออื่นเช่น task หรือ
promise) Rust ให้ trait Future เป็น building block เพื่อให้
operation async ต่าง ๆ ถูก implement ด้วยโครงสร้างข้อมูลต่าง ๆ แต่
ด้วย interface ทั่วไป ใน Rust future คือ type ที่ implement trait
Future แต่ละ future เก็บข้อมูลของตัวเองเกี่ยวกับความก้าวหน้าที่ทำ
และ “พร้อม” หมายความว่าอะไร
คุณใช้ keyword async กับ block และฟังก์ชันเพื่อระบุว่าพวกมันถูก
interrupt และ resume ได้ ภายใน block async หรือฟังก์ชัน async คุณ
ใช้ keyword await เพื่อ await future (นั่นคือ รอให้มันพร้อม)
ได้ จุดใดที่คุณ await future ภายใน block async หรือฟังก์ชันเป็นจุด
ที่เป็นไปได้สำหรับ block หรือฟังก์ชันนั้นที่จะ pause และ resume
กระบวนการของการตรวจสอบกับ future เพื่อดูว่าค่าของมันใช้ได้ยังเรียก
polling
บางภาษาอื่นเช่น C# และ JavaScript ก็ใช้ keyword async และ await
สำหรับ async programming ถ้าคุณคุ้นเคยกับภาษาเหล่านั้น คุณอาจสังเกต
ความแตกต่างสำคัญบางอย่างในวิธีที่ Rust จัดการ syntax นั่นมีเหตุผลที่
ดี ดังที่เราจะเห็น!
เมื่อเขียน async Rust เราใช้ keyword async และ await เกือบทุก
เวลา Rust คอมไพล์พวกมันเป็นโค้ดเทียบเท่าโดยใช้ trait Future เหมือน
ที่มันคอมไพล์ loop for เป็นโค้ดเทียบเท่าโดยใช้ trait Iterator
อย่างไรก็ตาม เพราะ Rust ให้ trait Future คุณยัง implement มัน
สำหรับ type ของคุณเองเมื่อต้องการได้ ฟังก์ชันหลายตัวที่เราจะเห็น
ตลอดบทนี้ return type ที่มี implementation ของ Future ของตัวเอง
เราจะกลับไปยังนิยามของ trait ที่ท้ายสุดของบทและขุดเข้าไปในวิธีที่
มันทำงานเพิ่ม แต่นี่เป็นรายละเอียดเพียงพอที่จะให้เราเดินหน้าต่อ
ทั้งหมดนี้อาจรู้สึก abstract หน่อย ดังนั้นมาเขียนโปรแกรม async แรก ของเรา — web scraper เล็ก เราจะส่งสอง URL จาก command line, fetch ทั้งสองแบบ concurrent และ return ผลของอันไหนเสร็จก่อน ตัวอย่างนี้จะ มี syntax ใหม่พอควร แต่ไม่ต้องกังวล — เราจะอธิบายทุกอย่างที่คุณต้อง รู้ขณะที่เราไป
โปรแกรม Async แรกของเรา
เพื่อเก็บโฟกัสของบทนี้ที่การเรียน async ไม่ใช่การ juggle ส่วนของ
ecosystem เราสร้าง crate trpl (trpl ย่อสำหรับ “The Rust
Programming Language”) มัน re-export type, trait และฟังก์ชันทั้งหมด
ที่คุณต้องการ ส่วนใหญ่จาก crate futures
และ tokio crate futures เป็นบ้านอย่างเป็น
ทางการสำหรับการทดลอง Rust สำหรับโค้ด async และมันจริง ๆ คือที่ที่
trait Future ถูกออกแบบดั้งเดิม Tokio เป็น async runtime ที่ใช้
อย่างกว้างขวางที่สุดใน Rust วันนี้ โดยเฉพาะสำหรับ web application
มี runtime อื่นที่ยอดเยี่ยมอยู่ และพวกมันอาจเหมาะกับจุดประสงค์ของ
คุณมากกว่า เราใช้ crate tokio ใต้ฝ่ามือสำหรับ trpl เพราะมันถูก
ทดสอบดีและใช้กว้างขวาง
ในบางกรณี trpl ยังตั้งชื่อใหม่หรือ wrap API ดั้งเดิมเพื่อเก็บคุณ
โฟกัสที่รายละเอียดที่เกี่ยวข้องกับบทนี้ ถ้าคุณต้องการเข้าใจว่า
crate ทำอะไร เราขอแนะนำให้คุณดู source code ของมัน
คุณจะเห็นได้ว่าแต่ละ re-export มาจาก crate ใด และเราได้ทิ้ง comment
มากที่อธิบายว่า crate ทำอะไร
สร้างโปรเจกต์ binary ใหม่ชื่อ hello-async และเพิ่ม crate trpl
เป็น dependency:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
ตอนนี้เราใช้ชิ้นส่วนต่าง ๆ ที่ trpl ให้มาเพื่อเขียนโปรแกรม async
แรกของเรา เราจะ build เครื่องมือ command line เล็กที่ fetch สอง
หน้าเว็บ ดึง element <title> จากแต่ละหน้า และ print title ของหน้า
ใดที่เสร็จกระบวนการทั้งหมดนั้นก่อน
นิยามฟังก์ชัน page_title
มาเริ่มโดยเขียนฟังก์ชันที่รับ URL หนึ่งหน้าเป็น parameter, ทำ request
ไปยังมัน และ return text ของ element <title> (ดู Listing 17-1)
extern crate trpl; // required for mdbook test
fn main() {
// TODO: we'll add this next!
}
use trpl::Html;
async fn page_title(url: &str) -> Option<String> {
let response = trpl::get(url).await;
let response_text = response.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
ก่อนอื่น เรานิยามฟังก์ชันชื่อ page_title และทำเครื่องหมายมันด้วย
keyword async จากนั้นเราใช้ฟังก์ชัน trpl::get เพื่อ fetch URL
ใดก็ตามที่ส่งเข้าและเพิ่ม keyword await เพื่อ await response เพื่อ
รับ text ของ response เราเรียกเมธอด text ของมันและ await มันอีก
ครั้งด้วย keyword await ทั้งสองขั้นตอนนี้เป็น asynchronous สำหรับ
ฟังก์ชัน get เราต้องรอ server ส่งคืนส่วนแรกของ response ซึ่งจะ
รวม HTTP header, cookie และอื่น ๆ และส่งแยกจาก body ของ response
ได้ โดยเฉพาะถ้า body ใหญ่มาก อาจใช้เวลาให้ทั้งหมดของมันมาถึง เพราะ
เราต้องรอ ทั้งหมด ของ response ที่มาถึง เมธอด text ยังเป็น async
เราต้อง await future ทั้งสองนี้ชัดเจน เพราะ future ใน Rust เป็น
lazy — พวกมันไม่ทำอะไรจนกว่าคุณขอพวกมันด้วย keyword await
(จริง ๆ Rust จะแสดง warning ของ compiler ถ้าคุณไม่ใช้ future) นี่
อาจทำให้คุณนึกถึงการพูดถึง iterator ในส่วน
“ประมวลผลชุด Item ด้วย Iterator”
ในบทที่ 13 Iterator ไม่ทำอะไรยกเว้นคุณเรียกเมธอด next ของพวกมัน —
ไม่ว่าโดยตรงหรือโดยใช้ loop for หรือเมธอดเช่น map ที่ใช้ next
ใต้ฝ่ามือ ในทำนองเดียวกัน future ไม่ทำอะไรยกเว้นคุณขอพวกมันชัดเจน
ความเป็น lazy นี้อนุญาตให้ Rust หลีกเลี่ยงการรันโค้ด async จนกว่ามัน
ต้องการจริง
สังเกต — นี่ต่างจากพฤติกรรมที่เราเห็นเมื่อใช้
thread::spawnใน ส่วน “สร้าง Thread ใหม่ด้วย spawn” ในบทที่ 16 ที่ closure ที่เราส่งไปยังอีกเธรดเริ่มรันทันที มันยัง ต่างจากวิธีที่ภาษาอื่นหลายภาษาเข้าหา async แต่มันสำคัญสำหรับ Rust ที่จะให้การรับประกัน performance ของมันได้ เช่นเดียวกับ iterator
เมื่อเรามี response_text แล้ว เรา parse มันเป็น instance ของ type
Html โดยใช้ Html::parse ได้ แทน raw string ตอนนี้เรามี type
ข้อมูลที่เราใช้ทำงานกับ HTML เป็นโครงสร้างข้อมูลที่ richer โดย
เฉพาะ เราใช้เมธอด select_first เพื่อหา instance แรกของ selector
CSS ที่ให้ โดยส่ง string "title" เราจะได้ element <title> แรก
ใน document ถ้ามี เพราะอาจไม่มี element ที่ตรง select_first
return Option<ElementRef> สุดท้าย เราใช้เมธอด Option::map ซึ่ง
ให้เราทำงานกับ item ใน Option ถ้ามี และไม่ทำอะไรถ้าไม่มี (เรา
ใช้ expression match ที่นี่ได้ด้วย แต่ map เป็น idiomatic
มากกว่า) ใน body ของฟังก์ชันที่เราให้ map เราเรียก inner_html
บน title เพื่อรับเนื้อหาของมัน ซึ่งเป็น String เมื่อทุกอย่าง
พูดและทำ เรามี Option<String>
สังเกตว่า keyword await ของ Rust ไป หลัง expression ที่คุณกำลัง
await ไม่ใช่ก่อน นั่นคือ มันเป็น keyword postfix นี่อาจต่างจาก
ที่คุณคุ้นเคยถ้าคุณใช้ async ในภาษาอื่น แต่ใน Rust มันทำให้ chain
ของเมธอดทำงานกับด้วยดีกว่ามาก ผลคือ เราเปลี่ยน body ของ page_title
ให้ chain การเรียกฟังก์ชัน trpl::get และ text ด้วย await
ระหว่างพวกมันได้ ดังที่แสดงใน Listing 17-2
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
// TODO: we'll add this next!
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
awaitด้วยนั้น เราเขียนฟังก์ชัน async แรกของเราสำเร็จแล้ว! ก่อนเราเพิ่ม
โค้ดใน main เพื่อเรียกมัน มาพูดเพิ่มเล็กน้อยเกี่ยวกับสิ่งที่เรา
เขียนและมันหมายความอะไร
เมื่อ Rust เห็น block ที่ทำเครื่องหมายด้วย keyword async มัน
คอมไพล์มันเป็นประเภทข้อมูล unique และ anonymous ที่ implement trait
Future เมื่อ Rust เห็น ฟังก์ชัน ที่ทำเครื่องหมายด้วย async
มันคอมไพล์มันเป็นฟังก์ชันที่ไม่ใช่ async ที่ body ของมันคือ block
async return type ของฟังก์ชัน async คือ type ของประเภทข้อมูล
anonymous ที่ compiler สร้างสำหรับ block async นั้น
ดังนั้น การเขียน async fn เทียบเท่ากับการเขียนฟังก์ชันที่ return
future ของ return type ต่อ compiler นิยามฟังก์ชันเช่น
async fn page_title ใน Listing 17-1 ประมาณเทียบเท่ากับฟังก์ชัน
ที่ไม่ใช่ async ที่นิยามแบบนี้:
#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
use std::future::Future;
use trpl::Html;
fn page_title(url: &str) -> impl Future<Output = Option<String>> {
async move {
let text = trpl::get(url).await.text().await;
Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html())
}
}
}
มาเดินผ่านแต่ละส่วนของเวอร์ชันที่แปลง:
- มันใช้ syntax
impl Traitที่เราพูดถึงในบทที่ 10 ในส่วน “Trait เป็น Parameter” - ค่าที่ return implement trait
Futureที่มี associated typeOutputสังเกตว่า typeOutputคือOption<String>ซึ่งเหมือน กับ return type ดั้งเดิมจากเวอร์ชันasync fnของpage_title - โค้ดทั้งหมดที่เรียกใน body ของฟังก์ชันดั้งเดิมถูก wrap ใน block
async moveจำได้ว่า block เป็น expression block ทั้งหมดนี้คือ expression ที่ return จากฟังก์ชัน - block async นี้สร้างค่ากับ type
Option<String>ดังที่อธิบาย ค่านั้นตรงกับ typeOutputใน return type นี่เหมือนกับ block อื่น ที่คุณเห็น - body ฟังก์ชันใหม่เป็น block
async moveเพราะวิธีที่มันใช้ parameterurl(เราจะพูดเพิ่มมากเกี่ยวกับasyncเทียบกับasync moveภายหลังในบท)
ตอนนี้เราเรียก page_title ใน main ได้
Execute ฟังก์ชัน Async ด้วย Runtime
เพื่อเริ่ม เราจะรับ title สำหรับหน้าเดียว แสดงใน Listing 17-3 โชค ไม่ดี โค้ดนี้ยังไม่คอมไพล์
extern crate trpl; // required for mdbook test
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
page_title จาก main กับอาร์กิวเมนต์ที่ user ให้เราตาม pattern เดียวกับที่เราใช้รับอาร์กิวเมนต์ command line ในส่วน
“รับ Command Line Argument” ในบทที่ 12
จากนั้นเราส่งอาร์กิวเมนต์ URL ให้ page_title และ await ผล เพราะ
ค่าที่ผลิตโดย future คือ Option<String> เราใช้ expression match
เพื่อ print ข้อความต่างกันเพื่อรองรับว่าหน้ามี <title> ไหม
ที่เดียวที่เราใช้ keyword await ได้คือในฟังก์ชันหรือ block async
และ Rust จะไม่ให้เราทำเครื่องหมายฟังก์ชันพิเศษ main เป็น async
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
เหตุผลที่ main ทำเครื่องหมาย async ไม่ได้คือโค้ด async ต้องการ
runtime — crate Rust ที่จัดการรายละเอียดของการ execute โค้ด
asynchronous ฟังก์ชัน main ของโปรแกรม initialize runtime ได้
แต่มันไม่ใช่ runtime เอง (เราจะเห็นเพิ่มเกี่ยวกับทำไมนี่เป็นกรณี
ในไม่ช้า) ทุกโปรแกรม Rust ที่ execute โค้ด async มีอย่างน้อยหนึ่งที่
ที่มันตั้ง runtime ที่ execute future
ภาษาส่วนใหญ่ที่สนับสนุน async bundle runtime แต่ Rust ไม่ แทน มี async runtime ต่างกันหลายตัวให้ใช้ แต่ละตัวทำ tradeoff ต่างกันเหมาะ สำหรับ use case ที่มันเป้าหมาย ตัวอย่างเช่น web server high-throughput กับหลาย CPU core และ RAM จำนวนมากมีความต้องการต่างมาก กับ microcontroller ที่มี core เดียว, RAM จำนวนน้อย และไม่มีความ สามารถ heap allocation crate ที่ให้ runtime เหล่านั้นยังมักจัดส่ง async version ของ functionality ทั่วไปเช่น I/O ไฟล์หรือ network
ที่นี่และตลอดที่เหลือของบทนี้ เราจะใช้ฟังก์ชัน block_on จาก crate
trpl ซึ่งรับ future เป็นอาร์กิวเมนต์และ block เธรดปัจจุบันจนกว่า
future นี้รันจนเสร็จ เบื้องหลัง การเรียก block_on ตั้ง runtime
โดยใช้ crate tokio ที่ใช้รัน future ที่ส่งเข้า (พฤติกรรม block_on
ของ crate trpl คล้ายกับฟังก์ชัน block_on ของ runtime crate อื่น)
เมื่อ future เสร็จ block_on return ค่าใดที่ future สร้าง
เราส่ง future ที่ return โดย page_title ให้ block_on โดยตรง
และเมื่อมันเสร็จ เรา match บน Option<String> ที่ได้ดังที่เราพยายาม
ทำใน Listing 17-3 ได้ อย่างไรก็ตาม สำหรับตัวอย่างส่วนใหญ่ในบท (และ
โค้ด async ส่วนใหญ่ในโลกจริง) เราจะทำมากกว่าเพียงการเรียกฟังก์ชัน
async หนึ่งครั้ง ดังนั้นแทน เราจะส่ง block async และ await ผลของ
การเรียก page_title ชัดเจน ดังใน Listing 17-4
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
trpl::block_onเมื่อเรารันโค้ดนี้ เราได้พฤติกรรมที่เราคาดหวังในตอนแรก:
$ cargo run -- "https://www.rust-lang.org"
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
Rust Programming Language
โอ — ในที่สุดเรามีโค้ด async ที่ทำงาน! แต่ก่อนเราเพิ่มโค้ดเพื่อแข่ง สองไซต์กัน มาหันความสนใจสั้น ๆ กลับไปยังวิธีที่ future ทำงาน
แต่ละ จุด await — นั่นคือ ทุกที่ที่โค้ดใช้ keyword await —
แทนที่ที่การควบคุมถูกส่งกลับให้ runtime เพื่อทำให้นั้นทำงาน Rust
ต้องตามว่า state ที่เกี่ยวข้องใน block async เพื่อให้ runtime เริ่ม
งานอื่นและจากนั้นกลับมาเมื่อมันพร้อมที่จะลองก้าวหน้าอันแรกอีก นี่
คือ state machine ที่มองไม่เห็น เหมือนกับว่าคุณเขียน enum แบบนี้
เพื่อบันทึก state ปัจจุบันที่แต่ละจุด await:
#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
enum PageTitleFuture<'a> {
Initial { url: &'a str },
GetAwaitPoint { url: &'a str },
TextAwaitPoint { response: trpl::Response },
}
}
การเขียนโค้ดเพื่อเปลี่ยนระหว่างแต่ละ state ด้วยมือจะน่าเบื่อและมี แนวโน้ม error อย่างไรก็ตาม โดยเฉพาะเมื่อคุณต้องเพิ่ม functionality และ state เพิ่มให้โค้ดภายหลัง โชคดี Rust compiler สร้างและจัดการ โครงสร้างข้อมูล state machine สำหรับโค้ด async อัตโนมัติ กฎ borrowing และ ownership ปกติรอบโครงสร้างข้อมูลทั้งหมดยังใช้ และโชค ดี compiler ยังจัดการการตรวจสอบเหล่านั้นให้เราและให้ข้อความ error ที่มีประโยชน์ เราจะทำงานผ่านบางส่วนของพวกนั้นภายหลังในบท
ในที่สุด อะไรบางอย่างต้อง execute state machine นี้ และอะไรบางอย่าง นั้นคือ runtime (นี่คือเหตุผลที่คุณอาจพบการกล่าวถึง executor เมื่อ ดูเข้า runtime — executor เป็นส่วนของ runtime ที่รับผิดชอบในการ execute โค้ด async)
ตอนนี้คุณเห็นได้ว่าทำไม compiler หยุดเราจากการทำ main เอง async
function กลับใน Listing 17-3 ถ้า main เป็นฟังก์ชัน async อะไรบาง
อย่างอื่นจะต้องจัดการ state machine สำหรับ future ใดที่ main
return แต่ main คือจุดเริ่มของโปรแกรม! แทน เราเรียกฟังก์ชัน
trpl::block_on ใน main เพื่อตั้ง runtime และรัน future ที่
return โดย block async จนกว่ามันเสร็จ
สังเกต — บาง runtime ให้ macro เพื่อคุณ สามารถ เขียนฟังก์ชัน
mainasync ได้ macro เหล่านั้นเขียนasync fn main() { ... }ใหม่เป็นfn mainปกติ ซึ่งทำสิ่งเดียวกับที่เราทำด้วยมือใน Listing 17-4 — เรียกฟังก์ชันที่รัน future จนเสร็จในแบบที่trpl::block_onทำ
ตอนนี้มาใส่ชิ้นส่วนเหล่านี้รวมกันและเห็นว่าเราเขียนโค้ด concurrent ยังไงได้
แข่งสอง URL แบบ Concurrent
ใน Listing 17-5 เราเรียก page_title กับสอง URL ต่างกันที่ส่งเข้า
จาก command line และแข่งพวกมันโดยเลือก future ใดที่เสร็จก่อน
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::select(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title was: '{title}'"),
None => println!("It had no title."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
page_title สำหรับสอง URL เพื่อดูว่าอันไหน return ก่อนเราเริ่มโดยเรียก page_title สำหรับแต่ละของ URL ที่ user ให้ เรา
บันทึก future ที่ได้เป็น title_fut_1 และ title_fut_2 จำได้ พวก
มันไม่ทำอะไรยัง เพราะ future เป็น lazy และเรายังไม่ได้ await พวก
มัน จากนั้นเราส่ง future ให้ trpl::select ซึ่ง return ค่าเพื่อ
ระบุว่าอันไหนของ future ที่ส่งให้มันเสร็จก่อน
สังเกต — ใต้ฝ่ามือ
trpl::selectถูก build บนฟังก์ชันselectทั่วไปกว่าที่นิยามใน cratefuturesฟังก์ชันselectของ cratefuturesทำสิ่งหลายอย่างที่ฟังก์ชันtrpl::selectทำไม่ได้ แต่ มันยังมีความซับซ้อนเพิ่มที่เราข้ามได้ตอนนี้
future ใดก็ “ชนะ” อย่างถูกต้องตามกฎหมายได้ ดังนั้นมันไม่สมเหตุสมผล
ที่จะ return Result แทน trpl::select return type ที่เรายังไม่
เห็นก่อน trpl::Either type Either คล้ายกับ Result หน่อยตรงที่
มันมีสองกรณี ต่างจาก Result อย่างไรก็ตาม ไม่มีแนวคิดของความสำเร็จ
หรือ failure ที่ฝังใน Either แทน มันใช้ Left และ Right เพื่อ
ระบุ “อันหนึ่งหรืออีกอัน”:
#![allow(unused)]
fn main() {
enum Either<A, B> {
Left(A),
Right(B),
}
}
ฟังก์ชัน select return Left กับ output ของ future นั้นถ้า
อาร์กิวเมนต์แรกชนะ และ Right กับ output ของอาร์กิวเมนต์ future
ที่สองถ้า อันนั้น ชนะ นี่ตรงลำดับที่อาร์กิวเมนต์ปรากฏเมื่อเรียก
ฟังก์ชัน — อาร์กิวเมนต์แรกอยู่ทางซ้ายของอาร์กิวเมนต์ที่สอง
เรายังอัพเดท page_title ให้ return URL เดียวกันที่ส่งเข้า ด้วย
วิธีนั้น ถ้าหน้าที่ return ก่อนไม่มี <title> ที่เรา resolve ได้
เรายัง print ข้อความที่มีความหมายได้ ด้วยข้อมูลนั้นใช้ได้ เราปิด
โดยอัพเดท output println! ของเราเพื่อระบุทั้งว่า URL ไหนเสร็จก่อน
และอะไร ถ้ามี <title> สำหรับหน้าเว็บที่ URL นั้น
คุณได้ build web scraper ที่ทำงานเล็กแล้ว! เลือกสอง URL และรัน เครื่องมือ command line คุณอาจค้นพบว่าบางไซต์เร็วกว่าอื่นอย่าง สม่ำเสมอ ในขณะที่ในบางกรณีไซต์ที่เร็วกว่าต่างกันจากการรันถึงการรัน สำคัญกว่า คุณเรียนพื้นฐานของการทำงานกับ future แล้ว ดังนั้นตอนนี้เรา ขุดลึกขึ้นในสิ่งที่เราทำกับ async ได้