async
/.await
توی فصل اول نگاه کوتاهی به async
/.await
داشتیم.
توی این فصل با جزئیات بیشتری روی async
/.await
بحث میکنیم و میبینم چجوری کد async با کد عادی که قبلا تو Rust میدیدیم فرق داره.
async
/.await
کلیدواژه های ویژه ای هستند که قدرت برنامه نویسی async رو به Rust میدن و اجازه میدن قدرت به thread برگرده و عملیات بعدی رو انجام بده در حالی که منتظر جواب یه کد async هست.
دو راه اصلی برای استفاده از async
وجود داره: یکی async fn
و اون یکی بلاک های async
هستن.
که هر کدوم مقداری رو بر میگردونن که trait Future
رو پیاده سازی کرده.
// تابع زیر تایپی رو برمیگردونه که "تریت" زیر رو پیاده سازی کرده:
// `Future<Output = u8>`
// که باعث میشه در نهایت تایپ زیر برگرده:
// u8
async fn foo() -> u8 { 5 }
fn bar() -> impl Future<Output = u8> {
// بلاک زیر تایپی رو برمیگردونه از نوع:
// `Future<Output = u8>`
async {
let x: u8 = foo().await;
x + 5
}
}
همونطور که توی فصل اول دیدیم بدنه async
و بقیه Future ها تنبل هستن:
به این معنی که تا زمانی که اجرا نشن هیچ کاری نمیکنن. معمول ترین روش اجرا کردن Future
اینه که منتظرش بمونید یا .await
اش کنید.
زمانی که .await
روی یه Future
صدا زده میشه سعی میکنه اونو اجراش کنه تا با موفقیت تموم بشه. اگه Future
بلاک کننده و مسدود کندده thread باشه کنترل رو به thread بر میگردونه. وقتی پردازش بیشتری میتونه انجام بشه Future
دوباره توسط اجرا کننده برداشته میشه و ادامه پردازشش انجام میشه و این باعث میشه .await
با موفقیت تکمیل بشه.
Lifetime های async
برعکس توابع معمول در Rust، توابع async fn
ای که رفرنس ها یا آرگومان های غیر از static
رو به عنوان ورودی میگیرن، Future
ای بر میگردونن که lifetime شون دقیقا مثل lifetime آرگومان هاشون هست:
// این تابع:
async fn foo(x: &u8) -> u8 { *x }
// با این تابع برابره:
fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
async move { *x }
}
که یعنی تابع async fn
ای که یک Future بر میگردونه باید توسط .await
صدا زده بشه در حالی که هنوز آرگومان های غیر static
اش موجود هستن.
تو حالت عادی موقعی که .await
میکنید بعد از اینکه تابع رو صدا میزنید مثلا مثل foo(&x).await
موردی به موجود نمیاد و مشکلی نیست.
با این حال، ذخیره کردن Future ها یا ارسالشون به یک task یا thread دیگه ممکنه مشکل ساز بشه.
یکی از کارهایی که برای تبدیل asyn fn
با آرگومان هایی از جنس رفرنس به Future های 'static
میشه انجام داد، اینه که آرگومان ها رو با صدا کردن async fn
داخل یک بلاک async
جا بدیم:
fn bad() -> impl Future<Output = u8> {
let x = 5;
borrow_x(&x) // ارور: مقدار "ایکس" زمان زیادی زنده نیست یعنی لایف تایمش اجازه نمیده
}
fn good() -> impl Future<Output = u8> {
async {
let x = 5;
borrow_x(&x).await
}
}
با انتقال آرگومان ها به بلاک async
، lifetime اون رو بسط دادیم به lifetime اون Future
ای که از صدا زدن good
برمگیرده.
async move
بلاک های async
و clouser مثل clouser های عادی اجازه استفاده از کلیدواژه move
رو میده.
یه بلاک async move
باعث میشه مالکیت تمام متغیر هایی که به عنوان رفرنس استفاده میشن رو بگیره و عمرشون دقیقا به اون scope محدود بشه که این قابلیت باعث میشه دیگه نشه ازشون تو قسمت های دیگه کد استفاده کرد:
/// `async` block:
///
/// چندین بلاک از نوع زیر میتونن به متغیر های لوکال دسترسی داشته باشن
/// البته تا وقتی که تو اون اسکوپ اجرا بشن
async fn blocks() {
let my_string = "foo".to_string();
let future_one = async {
// ...
println!("{my_string}");
};
let future_two = async {
// ...
println!("{my_string}");
};
// هر دو "فیوچر" اجرا میشه تا در نهایت دو بار مقدار زیر تپرینت میشه:
// "foo"
let ((), ()) = futures::join!(future_one, future_two);
}
/// `async move` block:
///
/// فقط یک بلاکی که کلیدواژه "موو" داره میتونه به مقادیر لوکال در لحظه دسترسی داشته باشه
/// به خاطر اینکه اون مقادیر به اسکوپ "فیوچر" منتقل شدن
/// البته همین باعث میشه اون "فیوچر" بیشتر از اسکوپ اصلی زنده بمونه و دووم بیاره:
fn move_block() -> impl Future<Output = ()> {
let my_string = "foo".to_string();
async move {
// ...
println!("{my_string}");
}
}
.await
کردن روی یک اجراکننده Multi-thread
دقت کنید که وقتی از یک اجرا کننده Future
به صورت Multi-thread استفاده میکنیم، Future
ها میتونن بین thread ها منتقل بشن پس هم این Future ها هم متغیر ها باید امکان انتقال بین thread ها رو داشته باشن، چون هر درخواست .await
میتونه منجر به تعویض thread بشه.
که این یعنی استفاده از Rc
یا &RefCell
یا تایپ های دیگه ای که trait Send
رو پیاده سازی نکردن و همچنین رفرنس هایی که به تایپ هایی اشاره دارن که trait Sync
رو پیاده سازی نکردن، امن نیست.
(احتیاط: امکانش وجود داره از این تایپ ها استفاده کنید البته تا زمانی که توی scope ای که .await
صدا زده میشه نباشن.)
به طور مشابه، ایده خوبی نیست مقادیر غیر Future ای lock شده رو توی .await
ها استفاده کنید، چون باعث lock شدن یا قفل شدن thread pool میشه: مثلا ممکنه یه task باعث lock بشه و .await
هم صدا زده بشه و قدرت کنترل برگرده به اجراکننده تا یه task دیگه انجام بشه که سعی کنه همون چیز رو lock کنه و همین باعث ایجاد deadlock بشه.
برای جلوگیری از این مشکل از تایپ Mutex
داخل future::lock
به جای std::sync
استفاده کنید.