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_element| title_element.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
در ایجاد یک Thread جدید باspawn
مشاهده کردیم، جایی که Closureی که به یک Thread دیگر ارسال کردیم بلافاصله شروع به اجرا کرد. همچنین، این رفتار با نحوه استفاده بسیاری از زبانهای دیگر از async متفاوت است. اما این برای Rust مهم است و بعداً خواهیم دید چرا.
وقتی response_text
را داریم، میتوانیم آن را با استفاده از Html::parse
به یک نمونه از نوع Html
تجزیه کنیم. به جای یک رشته خام، اکنون یک نوع داده داریم که میتوانیم از آن برای کار با HTML به عنوان یک ساختار داده غنیتر استفاده کنیم. به طور خاص، میتوانیم از متد select_first
برای پیدا کردن اولین نمونه از یک انتخابگر CSS خاص استفاده کنیم. با ارسال رشته "title"
، اولین عنصر <title>
در سند را دریافت خواهیم کرد، اگر وجود داشته باشد. چون ممکن است هیچ عنصر مطابقتی وجود نداشته باشد، select_first
یک Option<ElementRef>
بازمیگرداند. در نهایت، از متد Option::map
استفاده میکنیم که به ما اجازه میدهد با آیتم موجود در Option
کار کنیم، اگر موجود باشد، و اگر موجود نباشد، هیچ کاری انجام ندهیم. (میتوانستیم از یک عبارت match
هم استفاده کنیم، اما map
بیشتر idiomatic است.) در بدنه تابعی که به map
میدهیم، متد inner_html
را روی title_element
فراخوانی میکنیم تا محتوای آن را که یک String
است، دریافت کنیم. وقتی همه چیز انجام شد، یک Option<String>
خواهیم داشت.
توجه کنید که کلمه کلیدی await
در Rust بعد از عبارت مورد انتظار قرار میگیرد، نه قبل از آن. یعنی این یک کلمه کلیدی postfix است. این ممکن است با چیزی که به آن عادت دارید اگر از async در زبانهای دیگر استفاده کرده باشید، متفاوت باشد، اما در Rust این کار زنجیرهای از متدها را بسیار راحتتر میکند. در نتیجه، میتوانیم بدنه page_url_for
را تغییر دهیم تا فراخوانیهای تابع trpl::get
و text
را با await
بین آنها به هم زنجیر کنیم، همانطور که در لیست ۱۷-۲ نشان داده شده است.
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_element| title_element.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
که در فصل ۱۰ در بخش “ویژگیها به عنوان پارامتر” بحث کردیم، استفاده میکند. - ویژگی بازگرداندهشده یک
Future
با یک نوع وابسته به نامOutput
است. توجه کنید که نوعOutput
برابر باOption<String>
است، که همان نوع بازگشتی نسخه اصلیasync fn
تابعpage_title
است. - تمام کدی که در بدنه تابع اصلی فراخوانی شده است، در یک بلوک
async move
بستهبندی شده است. به یاد داشته باشید که بلوکها بیان (expression) هستند. این بلوک کامل، بیانی است که از تابع بازگردانده میشود. - این بلوک async یک مقداری با نوع
Option<String>
تولید میکند، همانطور که توضیح داده شد. این مقدار با نوعOutput
در نوع بازگشتی مطابقت دارد. این درست مانند بلوکهای دیگری است که قبلاً دیدهاید. - بدنه جدید تابع یک بلوک
async move
است به دلیل نحوه استفاده از پارامترurl
. (در ادامه فصل بیشتر درباره تفاوتasync
وasync move
صحبت خواهیم کرد.) - نسخه جدید تابع دارای نوعی طول عمر است که قبلاً ندیدهایم:
'_
. از آنجا که تابع یک future بازمیگرداند که به یک مرجع اشاره میکند—در این مورد، مرجعی که از پارامترurl
آمده است—باید به Rust بگوییم که میخواهیم آن مرجع شامل شود. نیازی نیست طول عمر را اینجا نامگذاری کنیم، زیرا Rust به اندازه کافی هوشمند است که بفهمد فقط یک مرجع میتواند درگیر باشد، اما باید صراحتاً مشخص کنیم که future حاصل به آن طول عمر محدود شده است.
حالا میتوانیم 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_element| title_element.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_element| title_element.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
خودش به عنوان یک تابع async در لیست ۱۷-۳ تعریف شود. اگر main
یک تابع async بود، چیزی دیگری باید ماشین حالت را برای futureی که main
بازمیگرداند مدیریت میکرد، اما main
نقطه شروع برنامه است! در عوض، ما تابع trpl::run
را در main
فراخوانی کردیم تا یک runtime راهاندازی کند و future بازگرداندهشده توسط بلوک async
را تا زمانی که Ready
بازگرداند، اجرا کند.
نکته: برخی runtimeها ماکروهایی ارائه میدهند که به شما اجازه میدهند یک تابع async برای
main
بنویسید. این ماکروهاasync fn main() { ... }
را به یکfn main
عادی تبدیل میکنند، که همان کاری را انجام میدهد که ما به صورت دستی در لیست ۱۷-۵ انجام دادیم: فراخوانی یک تابع که یک 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 is: '{title}'"),
None => println!("Its title could not be parsed."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let text = trpl::get(url).await.text().await;
let title = Html::parse(&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
مقدار Left
را با خروجی future اول بازمیگرداند اگر آرگومان اول برنده شود، و مقدار Right
را با خروجی future دوم بازمیگرداند اگر آن یکی برنده شود. این ترتیب با ترتیبی که آرگومانها هنگام فراخوانی تابع ظاهر میشوند مطابقت دارد: آرگومان اول در سمت چپ آرگومان دوم قرار دارد.
همچنین تابع page_title
را بهروزرسانی میکنیم تا همان URL ارسالشده را بازگرداند. به این ترتیب، اگر صفحهای که ابتدا بازمیگردد، دارای یک <title>
نباشد که بتوانیم آن را استخراج کنیم، همچنان میتوانیم یک پیام معنادار چاپ کنیم. با در دسترس بودن این اطلاعات، خروجی println!
خود را بهروزرسانی میکنیم تا مشخص کند کدام URL اول کامل شده است و <title>
صفحه وب در آن URL چیست (اگر وجود داشته باشد).
شما اکنون یک web scraper کوچک و کارا ساختهاید! چند URL انتخاب کنید و ابزار خط فرمان را اجرا کنید. ممکن است متوجه شوید که برخی سایتها به طور مداوم سریعتر از بقیه هستند، در حالی که در موارد دیگر، سایت سریعتر از اجرای به اجرای دیگر متفاوت است. مهمتر از همه، شما اصول کار با futures را آموختهاید، بنابراین حالا میتوانیم عمیقتر به آنچه میتوان با async انجام داد، بپردازیم.