Futures و سینتکس Async
عناصر کلیدی برنامهنویسی ناهمزمان در Rust شامل futures و کلمات کلیدی async و await هستند.
یک future مقداری است که ممکن است اکنون آماده نباشد، اما در آینده در نقطهای آماده خواهد شد. (این مفهوم در بسیاری از زبانها وجود دارد، گاهی با نامهای دیگر مانند task یا promise.) Rust یک ویژگی Future به عنوان یک بلوک سازنده فراهم میکند تا عملیاتهای async مختلف با ساختارهای داده متفاوت اما با یک رابط مشترک پیادهسازی شوند. در Rust، futures نوعهایی هستند که ویژگی Future را پیادهسازی میکنند. هر future اطلاعات خود را در مورد پیشرفت و اینکه “آماده” به چه معناست نگه میدارد.
میتوانید کلمه کلیدی async را به بلوکها و توابع اعمال کنید تا مشخص کنید که میتوانند متوقف شده و از سر گرفته شوند. درون یک بلوک async یا تابع async، میتوانید از کلمه کلیدی await برای انتظار یک future (یعنی منتظر ماندن تا آماده شود) استفاده کنید. هر نقطهای که در آن یک future را در یک بلوک یا تابع async انتظار میکشید، یک نقطه بالقوه برای متوقف و از سر گرفتن آن بلوک یا تابع async است. فرآیند بررسی یک future برای اینکه ببیند مقدار آن هنوز آماده است یا خیر، polling نامیده میشود.
برخی زبانهای دیگر، مانند C# و JavaScript، نیز از کلمات کلیدی async و await برای برنامهنویسی ناهمزمان استفاده میکنند. اگر با این زبانها آشنا هستید، ممکن است تفاوتهای قابل توجهی در نحوه عملکرد Rust، از جمله نحوه مدیریت سینتکس آن، مشاهده کنید. این تفاوتها دلایل خوبی دارند، همانطور که خواهیم دید!
هنگام نوشتن کد async در Rust، بیشتر اوقات از کلمات کلیدی async و await استفاده میکنیم. Rust آنها را به کدی معادل با استفاده از ویژگی Future کامپایل میکند، همانطور که حلقههای for را به کدی معادل با استفاده از ویژگی Iterator کامپایل میکند. با این حال، از آنجا که Rust ویژگی Future را ارائه میدهد، میتوانید آن را برای نوعهای داده خودتان نیز پیادهسازی کنید. بسیاری از توابعی که در طول این فصل مشاهده خواهیم کرد نوعهایی را بازمیگردانند که پیادهسازیهای خود از Future را دارند. در انتهای فصل به تعریف این ویژگی بازمیگردیم و بیشتر در مورد نحوه عملکرد آن بحث میکنیم، اما این توضیحات برای ادامه کافی است.
ممکن است این توضیحات کمی انتزاعی به نظر برسند، بنابراین بیایید اولین برنامه async خود را بنویسیم: یک web scraper کوچک. ما دو URL را از خط فرمان دریافت میکنیم، هر دو را به صورت همزمان دریافت میکنیم و نتیجه اولین URL که به پایان میرسد را بازمیگردانیم. این مثال دارای سینتکس جدیدی خواهد بود، اما نگران نباشید—همه چیزهایی که باید بدانید را در طول مسیر توضیح خواهیم داد.
اولین برنامه Async ما
برای تمرکز این فصل روی یادگیری async به جای مدیریت بخشهای اکوسیستم، یک crate به نام trpl ایجاد کردهایم (trpl مخفف “The Rust Programming Language” است). این crate همه نوعها، ویژگیها، و توابع مورد نیاز شما را بازصادر میکند، عمدتاً از crateهای futures و tokio. crate futures خانه رسمی برای آزمایش کد async در Rust است و در واقع جایی است که ویژگی Future در ابتدا طراحی شد. tokio امروز رایجترین Runtime async در Rust است، به ویژه برای برنامههای وب. Runtimeهای عالی دیگری نیز وجود دارند که ممکن است برای اهداف شما مناسبتر باشند. ما از crate tokio در زیرساخت trpl استفاده میکنیم زیرا به خوبی تست شده و به طور گسترده استفاده میشود.
در برخی موارد، trpl همچنین APIهای اصلی را تغییر نام داده یا آنها را پوشش میدهد تا شما را بر روی جزئیات مرتبط با این فصل متمرکز نگه دارد. اگر میخواهید بفهمید این crate چه میکند، ما شما را تشویق میکنیم که سورس کد آن را بررسی کنید. میتوانید ببینید که هر بازصادر از کدام crate میآید، و توضیحات گستردهای در مورد آنچه که crate انجام میدهد گذاشتهایم.
یک پروژه باینری جدید به نام hello-async ایجاد کنید و crate trpl را به عنوان وابستگی اضافه کنید:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
اکنون میتوانیم از بخشهای مختلف ارائهشده توسط trpl استفاده کنیم تا اولین برنامه async خود را بنویسیم. ما یک ابزار کوچک خط فرمان ایجاد خواهیم کرد که دو صفحه وب را دریافت میکند، عنصر <title> را از هرکدام استخراج میکند و عنوان صفحهای که سریعتر کل این فرآیند را تکمیل میکند، چاپ میکند.
تعریف تابع page_title
بیایید با نوشتن یک تابع که یک URL صفحه را به عنوان پارامتر میگیرد، یک درخواست به آن ارسال میکند و متن عنصر <title> را بازمیگرداند شروع کنیم (نگاه کنید به لیست ۱۷-۱).
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()) }
<title> از یک صفحه HTMLابتدا یک تابع به نام page_title تعریف میکنیم و آن را با کلمه کلیدی async علامتگذاری میکنیم. سپس از تابع trpl::get برای دریافت هر URL که به آن ارسال میشود استفاده میکنیم و کلمه کلیدی await را اضافه میکنیم تا منتظر پاسخ بمانیم. برای دریافت متن پاسخ، متد text را فراخوانی میکنیم و دوباره با کلمه کلیدی await منتظر آن میمانیم. هر دو این مراحل ناهمزمان هستند. برای تابع get، باید منتظر باشیم تا سرور اولین قسمت از پاسخ خود را ارسال کند که شامل هدرهای HTTP، کوکیها و غیره است و میتواند جدا از بدنه پاسخ ارسال شود. به ویژه اگر بدنه بسیار بزرگ باشد، ممکن است مدتی طول بکشد تا همه آن برسد. از آنجا که باید منتظر تمامیت پاسخ بمانیم، متد text نیز async است.
باید بهصراحت منتظر هر دو future باشیم، زیرا futures در Rust تنبل هستند: تا زمانی که از آنها با کلمه کلیدی await درخواست نشود، هیچ کاری انجام نمیدهند. (در واقع، Rust یک هشدار کامپایلر نمایش میدهد اگر از یک future استفاده نکنید.) این ممکن است شما را به یاد بحث فصل ۱۳ درباره iteratorها در بخش پردازش یک سری از آیتمها با iteratorها بیندازد. iteratorها هیچ کاری انجام نمیدهند مگر اینکه متد next آنها را فراخوانی کنید—چه به صورت مستقیم یا با استفاده از حلقههای for یا متدهایی مانند map که در پشت صحنه از next استفاده میکنند. به همین ترتیب، futures هیچ کاری انجام نمیدهند مگر اینکه بهصراحت از آنها درخواست شود. این ویژگی تنبلی به Rust اجازه میدهد تا کد async را تا زمانی که واقعاً مورد نیاز است، اجرا نکند.
توجه: این رفتار با چیزی که در فصل قبل هنگام استفاده از
thread::spawnدر ایجاد یک نخ جدید با spawn دیدیم متفاوت است، جایی که closureای که به نخ دیگر منتقل کردیم بلافاصله شروع به اجرا کرد. همچنین این رفتار با رویکرد بسیاری از زبانهای دیگر در مورد async نیز تفاوت دارد. اما این موضوع برای Rust اهمیت دارد تا بتواند تضمینهای عملکردی خود را همانند کاری که با پیمایشگرها انجام میدهد، حفظ کند.
زمانی که response_text را دریافت کردیم، میتوانیم آن را با استفاده از Html::parse به نمونهای از نوع Html تبدیل کنیم. به جای یک رشتهی خام، اکنون یک نوع داده داریم که میتوانیم از آن برای کار با HTML بهعنوان یک ساختار دادهی غنیتر استفاده کنیم. بهویژه میتوانیم از متد select_first برای یافتن اولین نمونه از یک سلکتور CSS مشخص استفاده کنیم. با ارسال رشتهی "title"، اولین عنصر <title> موجود در سند را دریافت خواهیم کرد، اگر عنصری وجود داشته باشد. از آنجایی که ممکن است هیچ عنصر مطابقتیافتهای وجود نداشته باشد، select_first یک Option<ElementRef> بازمیگرداند. در نهایت، از متد Option::map استفاده میکنیم که به ما اجازه میدهد اگر مقداری در Option وجود داشت با آن کار کنیم، و اگر وجود نداشت، هیچ کاری انجام ندهیم. (میتوانستیم از یک عبارت match نیز استفاده کنیم، اما استفاده از map در اینجا ایدیاتیکتر است.) در بدنهی تابعی که به map میدهیم، متد inner_html را روی title فراخوانی میکنیم تا محتوای آن را بهصورت یک String دریافت کنیم. در پایان، نتیجهی ما یک Option<String> خواهد بود.
توجه داشته باشید که کلمهی کلیدی await در Rust پس از عبارتی که منتظر آن هستید میآید، نه قبل از آن. به عبارت دیگر، این یک کلمهی کلیدی پسوندی است. این ممکن است با چیزی که در زبانهای دیگر هنگام استفاده از async تجربه کردهاید متفاوت باشد، اما در Rust این موضوع باعث میشود زنجیرههای توابع خواناتر و قابلمدیریتتر شوند. بنابراین، میتوانیم بدنهی تابع page_title را طوری تغییر دهیم که توابع trpl::get و text را با استفاده از await بین آنها بهصورت زنجیرهای صدا بزنیم، همانطور که در لیست 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 یک بلوک که با کلمه کلیدی async علامتگذاری شده است را میبیند، آن را به یک نوع داده منحصربهفرد و ناشناس که ویژگی Future را پیادهسازی میکند، کامپایل میکند. هنگامی که Rust یک تابع که با async علامتگذاری شده است را میبیند، آن را به یک تابع غیر-async که بدنه آن یک بلوک async است، کامپایل میکند. نوع بازگشتی یک تابع async نوع داده ناشناسی است که کامپایلر برای آن بلوک async ایجاد میکند.
بنابراین، نوشتن async fn معادل نوشتن تابعی است که یک future از نوع بازگشتی برمیگرداند. برای کامپایلر، یک تعریف تابع مانند async fn page_title در لیست ۱۷-۱ معادل یک تابع غیر-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()) } } }
بیایید هر بخش از نسخه تبدیلشده را بررسی کنیم:
- این تابع از سینتکس
impl Traitاستفاده میکند که در فصل ۱۰ در بخش [«Traits بهعنوان پارامتر»][impl-trait] بررسی کردیم. - trait بازگشتی یک
Futureاست با نوع مرتبطی به نامOutput. دقت کنید که نوعOutputمقدارOption<String>است، که همان نوع بازگشتی نسخهی اصلیasync fnتابعpage_titleمیباشد. - تمام کدی که در بدنهی تابع اصلی فراخوانی میشد، اکنون درون یک بلاک
async moveقرار گرفته است. به یاد داشته باشید که بلاکها در Rust یک عبارت محسوب میشوند. این بلاک بهطور کامل همان عبارتی است که از تابع بازگردانده میشود. - این بلاک async یک مقدار با نوع
Option<String>تولید میکند، همانطور که توصیف شد. این مقدار با نوعOutputدر نوع بازگشتی مطابقت دارد. این موضوع مشابه بلاکهای دیگری است که تاکنون دیدهاید. - بدنهی تابع جدید یک بلاک
async moveاست، به دلیل نحوهی استفاده از پارامترurlدرون بلاک. (در ادامهی این فصل، بهطور مفصلتر دربارهی تفاوتasyncوasync moveصحبت خواهیم کرد.)
حالا میتوانیم page_title را در main فراخوانی کنیم.
تعیین عنوان یک صفحه
برای شروع، فقط عنوان یک صفحه را دریافت میکنیم. در لیست ۱۷-۳، همان الگویی که در فصل ۱۲ برای دریافت آرگومانهای خط فرمان در بخش پذیرفتن آرگومانهای خط فرمان استفاده کردیم را دنبال میکنیم. سپس URL اول را به page_title ارسال کرده و نتیجه را انتظار میکشیم. چون مقداری که توسط future تولید میشود یک Option<String> است، از یک عبارت match برای چاپ پیامهای مختلف استفاده میکنیم تا مشخص شود آیا صفحه یک <title> داشته است یا خیر.
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 function from main with a user-supplied argumentمتأسفانه، این کد کامپایل نمیشود. تنها جایی که میتوانیم از کلمه کلیدی await استفاده کنیم، در توابع یا بلوکهای 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 که جزئیات اجرای کد ناهمزمان را مدیریت میکند. تابع main یک برنامه میتواند یک runtime را مقداردهی اولیه کند، اما خودش یک runtime نیست. (در ادامه، بیشتر خواهیم دید که چرا اینگونه است.) هر برنامه Rust که کد async اجرا میکند، حداقل یک مکان دارد که در آن یک runtime راهاندازی کرده و futures را اجرا میکند.
بیشتر زبانهایی که از async پشتیبانی میکنند، یک runtime همراه دارند، اما Rust این کار را نمیکند. در عوض، بسیاری از runtimeهای async مختلف موجود هستند که هرکدام موازنههای متفاوتی برای موارد استفاده خاص خود ارائه میدهند. برای مثال، یک وب سرور با توان عملیاتی بالا که دارای هستههای CPU متعدد و مقدار زیادی RAM است، نیازهای بسیار متفاوتی نسبت به یک میکروکنترلر با یک هسته، مقدار کمی RAM و بدون قابلیت تخصیص heap دارد. crateهایی که این runtimeها را فراهم میکنند اغلب نسخههای async از قابلیتهای عمومی مانند I/O فایل یا شبکه را نیز ارائه میدهند.
اینجا و در بقیه این فصل، از تابع run از crate trpl استفاده خواهیم کرد، که یک future را بهعنوان آرگومان میگیرد و آن را تا پایان اجرا میکند. در پشت صحنه، فراخوانی run یک runtime راهاندازی میکند که برای اجرای future ارسالشده استفاده میشود. وقتی future کامل شد، run هر مقداری که future تولید کرده باشد، بازمیگرداند.
میتوانستیم future بازگرداندهشده توسط page_title را مستقیماً به run ارسال کنیم، و وقتی کامل شد، میتوانستیم بر اساس Option<String> نتیجه، یک match انجام دهیم، همانطور که در لیست ۱۷-۳ تلاش کردیم. با این حال، برای بیشتر مثالهای این فصل (و بیشتر کد async در دنیای واقعی)، بیش از یک فراخوانی تابع async انجام خواهیم داد، بنابراین بهجای آن یک بلوک async ارسال میکنیم و صراحتاً نتیجه فراخوانی page_title را انتظار میکشیم، همانطور که در لیست ۱۷-۴ نشان داده شده است.
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(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::runوقتی این کد را اجرا میکنیم، رفتاری را که ممکن است ابتدا انتظار داشتیم دریافت میکنیم:
$ 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 کارا داریم! اما قبل از اینکه کدی اضافه کنیم که دو سایت را در مقابل یکدیگر رقابت دهد، بیایید بهطور مختصر دوباره به نحوه کار futures توجه کنیم.
هر نقطه انتظار—یعنی هر جایی که کد از کلمه کلیدی await استفاده میکند—نمایانگر جایی است که کنترل به runtime بازمیگردد. برای اینکه این کار انجام شود، Rust نیاز دارد وضعیت مربوط به بلوک async را پیگیری کند تا runtime بتواند کار دیگری را آغاز کند و سپس وقتی آماده شد دوباره برای پیشرفت بلوک اول بازگردد. این یک ماشین حالت نامرئی است، گویی که شما یک enum مانند این نوشتهاید تا وضعیت فعلی را در هر نقطه انتظار ذخیره کند:
#![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 }, } }
نوشتن کدی که به صورت دستی بین هر حالت انتقال یابد خستهکننده و مستعد خطا خواهد بود، بهویژه زمانی که بخواهید عملکرد بیشتری اضافه کرده و حالات بیشتری به کد اضافه کنید. خوشبختانه، کامپایلر Rust به طور خودکار ساختارهای داده مربوط به ماشین حالت را برای کد async ایجاد و مدیریت میکند. قوانین عادی مالکیت و قرضگیری در مورد ساختارهای داده همچنان اعمال میشوند، و خوشبختانه، کامپایلر بررسی این موارد را نیز برای ما انجام میدهد و پیامهای خطای مفیدی ارائه میدهد. در ادامه فصل چند مورد از این پیامها را بررسی خواهیم کرد.
در نهایت، چیزی باید این ماشین حالت را اجرا کند، و آن چیز یک runtime است. (به همین دلیل ممکن است در بررسی runtimeها به ارجاعاتی به executors برخورد کنید: یک executor بخشی از runtime است که مسئول اجرای کد async است.)
اکنون میتوانید دلیل اینکه چرا کامپایلر اجازه نداد تابع main را در لیستینگ 17-3 بهصورت async تعریف کنیم، بهتر درک کنید. اگر main یک تابع async بود، باید یک جزء دیگر مسئول مدیریت ماشین حالت برای futureای میبود که main بازمیگرداند؛ اما main نقطهی شروع برنامه است! بنابراین، بهجای آن در تابع main، تابع trpl::run را فراخوانی کردیم تا یک runtime راهاندازی کند و future بازگرداندهشده از بلاک async را تا زمان اتمام اجرا کند.
نکته: برخی runtimeها ماکروهایی فراهم میکنند که به شما اجازه میدهند یک تابع
mainبهصورت async بنویسید. این ماکروها عبارتasync fn main() { ... }را بازنویسی میکنند به یک تابعfn mainمعمولی که همان کاری را انجام میدهد که ما در لیستینگ 17-4 بهصورت دستی انجام دادیم: فراخوانی تابعی که یک future را تا تکمیل اجرا میکند، مانند کاری کهtrpl::runانجام میدهد.
حالا بیایید این بخشها را کنار هم قرار دهیم و ببینیم چگونه میتوان کدی همزمان نوشت.
رقابت بین دو URL
در لیست ۱۷-۵، ما page_title را با دو URL مختلف که از خط فرمان ارسال شدهاند، فراخوانی کرده و آنها را با یکدیگر رقابت میدهیم.
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::race(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هایی که توسط کاربر ارسال شدهاند، شروع میکنیم. Futureهای حاصل را به نامهای title_fut_1 و title_fut_2 ذخیره میکنیم. به یاد داشته باشید، اینها هنوز کاری انجام نمیدهند، زیرا futures تنبل هستند و هنوز منتظر آنها نماندهایم. سپس این futures را به trpl::race ارسال میکنیم، که مقداری بازمیگرداند تا نشان دهد کدام یک از futures ارسالشده به آن ابتدا کامل شده است.
نکته: در پشت صحنه،
raceبر اساس یک تابع عمومیتر به نامselectساخته شده است، که اغلب در کدهای واقعی Rust با آن مواجه خواهید شد. یک تابعselectمیتواند کارهایی انجام دهد که تابعtrpl::raceنمیتواند، اما همچنین دارای پیچیدگیهای اضافی است که فعلاً میتوانیم از آن صرفنظر کنیم.
هرکدام از futures میتوانند به طور قانونی “برنده” شوند، بنابراین بازگرداندن یک Result منطقی نیست. در عوض، race نوعی را بازمیگرداند که قبلاً ندیدهایم: trpl::Either. نوع Either تا حدودی شبیه به Result است به این معنا که دو حالت دارد. اما برخلاف Result، هیچ مفهومی از موفقیت یا شکست در Either وجود ندارد. در عوض، از Left و Right برای نشان دادن “یکی یا دیگری” استفاده میکند:
#![allow(unused)] fn main() { enum Either<A, B> { Left(A), Right(B), } }
تابع race در صورتی که اولین future ارائهشده زودتر به پایان برسد، مقدار Left را همراه با خروجی آن بازمیگرداند، و اگر دومین future زودتر به پایان برسد، مقدار Right را همراه با خروجی آن بازمیگرداند. این رفتار با ترتیبی که آرگومانها هنگام فراخوانی تابع ظاهر میشوند مطابقت دارد: آرگومان اول در سمت چپ آرگومان دوم قرار دارد.
همچنین تابع page_title را بهروزرسانی میکنیم تا همان URL ارسالشده را بازگرداند. به این ترتیب، اگر صفحهای که ابتدا بازمیگردد، دارای یک <title> نباشد که بتوانیم آن را استخراج کنیم، همچنان میتوانیم یک پیام معنادار چاپ کنیم. با در دسترس بودن این اطلاعات، خروجی println! خود را بهروزرسانی میکنیم تا مشخص کند کدام URL اول کامل شده است و <title> صفحه وب در آن URL چیست (اگر وجود داشته باشد).
شما اکنون یک web scraper کوچک و کارا ساختهاید! چند URL انتخاب کنید و ابزار خط فرمان را اجرا کنید. ممکن است متوجه شوید که برخی سایتها به طور مداوم سریعتر از بقیه هستند، در حالی که در موارد دیگر، سایت سریعتر از اجرای به اجرای دیگر متفاوت است. مهمتر از همه، شما اصول کار با futures را آموختهاید، بنابراین حالا میتوانیم عمیقتر به آنچه میتوان با async انجام داد، بپردازیم.