Rust

روز ۴۴: برنامه‌نویسی Async/Await در Rust و تفاوت آن با Threadها

با بزرگ‌تر شدن نرم‌افزارها، مخصوصاً در حوزه‌ی وب، شبکه و I/O، استفاده از Thread به‌تنهایی دیگر پاسخ‌گو نیست. ساخت تعداد زیاد Thread هزینه‌ی حافظه و Context Switch بالایی دارد و به‌راحتی می‌تواند عملکرد سیستم را کاهش دهد. Rust با معرفی Async/Await یک مدل مدرن و بسیار کارآمد برای مدیریت همزمانی ارائه می‌دهد که تمرکز آن روی استفاده‌ی بهینه از منابع و مقیاس‌پذیری بالاست. در این درس یاد می‌گیریم Async/Await در Rust دقیقاً چیست، چگونه کار می‌کند و چه تفاوتی با Thread دارد.

Async در Rust یعنی چه؟

Async در Rust به معنای اجرای همزمان چند Task بدون اشغال Thread به‌صورت بلاک‌کننده است. برخلاف Thread که معمولاً یک واحد سنگین از اجرای سیستم‌عامل است، Taskهای Async سبک هستند و روی تعداد محدودی Thread اجرا می‌شوند. زمانی که یک Task منتظر عملیات I/O می‌ماند، Thread آزاد می‌شود تا Task دیگری اجرا شود. این مدل باعث می‌شود هزاران یا حتی میلیون‌ها عملیات همزمان با مصرف منابع بسیار کمتر اجرا شوند.

در Rust، Async بخشی از Type System است و نه یک ویژگی جادویی در Runtime. هر تابع async در واقع یک Future برمی‌گرداند که نشان‌دهنده‌ی کاری است که در آینده کامل می‌شود.

async و await چگونه کار می‌کنند؟

کلمه‌ی کلیدی async برای تعریف توابع یا بلاک‌هایی استفاده می‌شود که نتیجه‌ی آن‌ها در آینده آماده می‌شود. await برای متوقف کردن اجرای Task تا زمانی که Future آماده شود به کار می‌رود، بدون اینکه Thread بلاک شود. این تفاوت مهم‌ترین مزیت Async نسبت به مدل‌های سنتی است.

async fn fetch_data() -> String {
    "Hello Async Rust".to_string()
}

async fn run() {
    let result = fetch_data().await;
    println!("{}", result);
}

در این مثال، fetch_data بلافاصله اجرا نمی‌شود، بلکه یک Future تولید می‌کند. اجرای واقعی زمانی رخ می‌دهد که Future توسط Executor پول شود.

Executor چیست و چرا به آن نیاز داریم؟

برخلاف برخی زبان‌ها، Rust به‌صورت پیش‌فرض Executor ندارد. Executor مسئول اجرای Futureها و مدیریت زمان‌بندی Taskهای Async است. کتابخانه‌هایی مثل Tokio و async-std این نقش را بر عهده دارند. این طراحی آگاهانه باعث می‌شود Rust سبک بماند و توسعه‌دهنده بتواند بسته به نیاز پروژه، Runtime مناسب را انتخاب کند.

#[tokio::main]
async fn main() {
    run().await;
}

در این مثال، Tokio Executor مسئول اجرای Futureهاست و main نیز به‌صورت async تعریف شده است.

تفاوت Async/Await با Thread در Rust

Threadها برای کارهای CPU-bound مناسب‌اند، اما Async برای I/O-bound طراحی شده است. وقتی برنامه‌ای تعداد زیادی درخواست شبکه، دیتابیس یا فایل را مدیریت می‌کند، Async بسیار کارآمدتر از Thread است. در Async، تعداد کمی Thread می‌توانند هزاران Task را مدیریت کنند، در حالی که در مدل Thread-Based، هر Thread هزینه‌ی قابل توجهی دارد.

از نظر ذهنی، Thread یعنی «چند کار همزمان»، اما Async یعنی «چند کار معلق که هرکدام وقتی آماده شدند ادامه پیدا می‌کنند». این تفاوت باعث می‌شود طراحی سیستم‌های مقیاس‌پذیر در Rust بسیار ساده‌تر و قابل کنترل‌تر شود.

Ownership و Borrowing در Async Rust

یکی از چالش‌های Async در Rust، تعامل آن با Ownership و Lifetimes است. چون Taskهای Async ممکن است بین Contextهای مختلف جابه‌جا شوند، Rust سخت‌گیری بیشتری روی داده‌هایی که در async fn استفاده می‌شوند اعمال می‌کند. به همین دلیل، بسیاری از Futureها نیاز دارند که داده‌هایشان ‘static باشد یا از Arc استفاده شود.

این سخت‌گیری در ابتدا ممکن است آزاردهنده به نظر برسد، اما نتیجه‌ی آن کدی است که از نظر حافظه و همزمانی به‌شدت ایمن است و در Runtime دچار رفتار غیرمنتظره نمی‌شود.

مثال عملی Async با Tokio

use tokio::time::{sleep, Duration};

async fn task_one() {
    sleep(Duration::from_secs(1)).await;
    println!("Task one done");
}

async fn task_two() {
    sleep(Duration::from_secs(2)).await;
    println!("Task two done");
}

#[tokio::main]
async fn main() {
    tokio::join!(task_one(), task_two());
}

در این مثال، دو Task به‌صورت همزمان اجرا می‌شوند بدون اینکه Thread بلاک شود. Tokio Executor به‌صورت هوشمند Taskها را زمان‌بندی می‌کند.

Async Rust و دنیای واقعی

Async/Await ستون اصلی فریم‌ورک‌های وب Rust مثل Actix و Axum است. سرورهای HTTP، WebSocket، ارتباط با دیتابیس و سیستم‌های پیام‌رسان همگی از Async استفاده می‌کنند. بدون Async، ساخت یک وب‌سرور مقیاس‌پذیر در Rust عملاً غیرممکن یا بسیار پرهزینه می‌شد. به همین دلیل، یادگیری Async/Await برای هر Rust Developer حرفه‌ای یک الزام است.

محدودیت‌ها و نکات مهم Async

Async همه‌جا بهترین انتخاب نیست. برای کارهای سنگین CPU-bound، استفاده از Thread یا Thread Pool منطقی‌تر است. همچنین Debug کردن کدهای Async می‌تواند پیچیده‌تر باشد. در Rust، مرز بین Async و Sync کاملاً مشخص است و توسعه‌دهنده باید آگاهانه تصمیم بگیرد کدام مدل را استفاده کند.

جمع‌بندی نهایی

Async/Await در Rust یک ابزار قدرتمند برای ساخت نرم‌افزارهای مدرن، مقیاس‌پذیر و کارآمد است. این مدل با ترکیب Type System قوی، Ownership و Executorهای حرفه‌ای، سطحی از ایمنی و کنترل را ارائه می‌دهد که در کمتر زبانی دیده می‌شود. اگر Threadها ابزار قدرت خام هستند، Async ابزار ظرافت و مقیاس‌پذیری است. تسلط بر Async/Await شما را آماده‌ی ورود به دنیای Rust در مقیاس صنعتی می‌کند.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

چهار × پنج =

دکمه بازگشت به بالا