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 انجام داد، بپردازیم.