مقدمه

به فصل «برنامه‌نویسی غیرهمزمان» خوش آمدید. «همزمانی» یا Concurrency به قابلیت یک برنامه برای اجرای چندین بخش از کد به صورت مستقل و خارج از ترتیب خطی اشاره دارد. در یک سیستم کامپیوتری مدرن با چندین هسته پردازشی، می‌توان این بخش‌ها را به صورت واقعاً موازی (parallel) اجرا کرد تا عملکرد کلی برنامه افزایش یابد. Rust با تکیه بر سیستم مالکیت و ایمنی نوع، ابزارهای قدرتمند و بسیار ایمنی را برای نوشتن کدهای همزمان فراهم می‌کند که بسیاری از خطاهای رایج در این حوزه (مانند race conditions) را در زمان کامپایل حذف می‌کند.

اولین و بنیادی‌ترین ابزار برای دستیابی به همزمانی، استفاده از «ترِدها» (Threads) است.

ایجاد یک ترِد جدید با thread::spawn

برای ایجاد یک ترِد جدید در Rust، از تابع thread::spawn استفاده می‌کنیم. این تابع یک کلوژر (closure) را به عنوان آرگومان دریافت می‌کند که حاوی کدی است که می‌خواهیم در ترِد جدید اجرا شود.

Copy Icon src/main.rs
use std::{thread, time::Duration};

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

اگر این کد را اجرا کنید، خواهید دید که خروجی دو حلقه به صورت درهم و غیرقابل پیش‌بینی چاپ می‌شود. این نشان می‌دهد که ترِد اصلی و ترِد جدید به صورت همزمان در حال اجرا هستند. نکته جالب دیگر این است که ترِد جدید قبل از اینکه تمام ۱۰ تکرار خود را تمام کند، متوقف می‌شود. این به این دلیل است که وقتی اجرای ترِد main به پایان می‌رسد، کل برنامه بسته شده و تمام ترِدهای دیگر نیز از بین می‌روند.

منتظر ماندن برای اتمام ترِدها با Join Handles

برای حل مشکل قبلی، باید ترِد اصلی را مجبور کنیم تا منتظر بماند تا ترِد جدید کار خود را به اتمام برساند. تابع thread::spawn یک «دستگیره اتصال» یا Join Handle از نوع JoinHandle<T> را برمی‌گرداند. ما می‌توانیم متد .join() را روی این دستگیره فراخوانی کنیم. این کار، اجرای ترِد فعلی را تا زمانی که ترِد مربوط به آن دستگیره به پایان برسد، مسدود (block) خواهد کرد.

Copy Icon src/main.rs
use std::{thread, time::Duration};

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap(); // Wait for the spawned thread to finish
}

اکنون با اجرای این کد، می‌بینید که ابتدا حلقه ترِد اصلی تمام می‌شود و سپس برنامه منتظر می‌ماند تا حلقه ترِد جدید نیز به طور کامل اجرا شود.

استفاده از move در کلوژرها

کلوژرها می‌توانند مقادیر را از محیط خود به ارث ببرند. اما وقتی یک کلوژر را به یک ترِد جدید منتقل می‌کنیم، کامپایلر Rust نمی‌داند که طول عمر آن ترِد چقدر خواهد بود. بنابراین، برای جلوگیری از ایجاد رفرنس‌های آویزان، Rust ما را مجبور می‌کند تا با استفاده از کلمه کلیدی move، مالکیت مقادیری که در کلوژر استفاده می‌شوند را به خود کلوژر منتقل کنیم.

Copy Icon src/main.rs
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    // The `move` keyword forces the closure to take ownership of `v`
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

کلمه کلیدی move قبل از لیست پارامترهای کلوژر، به Rust می‌گوید که مالکیت تمام متغیرهای به ارث برده شده (در اینجا v) را به داخل کلوژر منتقل کند. این کار تضمین می‌کند که حتی اگر ترِد اصلی زودتر به پایان برسد، داده‌ها همچنان برای ترِد جدید معتبر باقی می‌مانند.

در این درس با اصول اولیه ایجاد و مدیریت ترِدها در Rust آشنا شدیم. دیدیم که چگونه می‌توان با thread::spawn کدی را به صورت موازی اجرا کرد و چگونه با استفاده از join handles اجرای ترِدها را هماهنگ کرد. اما چگونه ترِدها می‌توانند با یکدیگر ارتباط برقرار کرده و داده‌ها را به اشتراک بگذارند؟ در درس بعدی، به بررسی «انتقال داده بین ترِدها» با استفاده از کانال‌ها خواهیم پرداخت.