Rust

روز ۴۲: برنامه‌نویسی Concurrency در Rust و فلسفه‌ی ایمنی ذاتی

Concurrency یکی از مهم‌ترین دلایلی است که بسیاری از توسعه‌دهندگان به Rust علاقه‌مند می‌شوند. در اغلب زبان‌ها، برنامه‌نویسی همزمان منبعی بی‌پایان از باگ‌های عجیب، Race Condition و Crashهای غیرقابل پیش‌بینی است. Rust از همان ابتدا با یک فلسفه‌ی متفاوت وارد این حوزه شده و تلاش می‌کند بسیاری از خطاهای رایج در Concurrency را در زمان کامپایل حذف کند. در این درس، با Threadها و Mutex آشنا می‌شویم و می‌بینیم Rust چگونه همزمانی را به شکلی ایمن و قابل اعتماد پیاده‌سازی می‌کند.

Concurrency چیست و چرا خطرناک است؟

Concurrency به معنی اجرای همزمان چند بخش از برنامه است که می‌توانند روی داده‌های مشترک کار کنند. مشکل اصلی زمانی ایجاد می‌شود که چند Thread به‌صورت همزمان به یک منبع مشترک دسترسی داشته باشند و حداقل یکی از آن‌ها قصد تغییر آن داده را داشته باشد. این وضعیت می‌تواند باعث Data Race شود؛ حالتی که نتیجه‌ی اجرای برنامه غیرقابل پیش‌بینی است. Rust با استفاده از Ownership، Borrowing و Type System بسیار سخت‌گیرانه، جلوی بسیاری از این خطاها را پیش از اجرا می‌گیرد.

ایجاد Thread در Rust

Rust برای ایجاد Thread از ماژول std::thread استفاده می‌کند. ساخت Thread در Rust بسیار ساده است اما قوانین Ownership همچنان به‌شدت اعمال می‌شوند. هر داده‌ای که وارد Thread جدید می‌شود باید یا منتقل شود یا به‌شکلی امن به اشتراک گذاشته شود. این طراحی باعث می‌شود برنامه‌نویس از همان ابتدا درباره‌ی مالکیت داده‌ها در محیط همزمان تصمیم‌گیری آگاهانه داشته باشد.

use std::thread;

let handle = thread::spawn(|| {
    println!("Hello from a new thread!");
});

handle.join().unwrap();

در این مثال، Thread جدید ایجاد شده و با استفاده از join منتظر پایان اجرای آن می‌مانیم تا برنامه قبل از اتمام Thread خاتمه پیدا نکند.

انتقال Ownership به Threadها

یکی از تفاوت‌های مهم Rust با زبان‌هایی مثل Java یا Python این است که داده‌ها به‌صورت پیش‌فرض قابل اشتراک نیستند. اگر بخواهیم داده‌ای را وارد Thread کنیم، باید Ownership آن را منتقل کنیم. این کار معمولاً با استفاده از move انجام می‌شود. این اجبار باعث می‌شود Data Race به‌صورت پیش‌فرض غیرممکن شود.

let data = vec![1, 2, 3];

let handle = thread::spawn(move || {
    println!("{:?}", data);
});

handle.join().unwrap();

پس از این کار، دیگر نمی‌توان به data در Thread اصلی دسترسی داشت و این دقیقاً همان چیزی است که Rust برای ایمنی حافظه می‌خواهد.

داده‌ی مشترک و نیاز به Mutex

در بسیاری از سناریوهای واقعی، نیاز داریم چند Thread به یک داده‌ی مشترک دسترسی داشته باشند و آن را تغییر دهند. اینجاست که Mutex وارد می‌شود. Mutex یا Mutual Exclusion ابزاری است که تضمین می‌کند در هر لحظه فقط یک Thread به داده دسترسی قابل تغییر دارد. Rust Mutex را به‌شکلی طراحی کرده که استفاده‌ی نادرست از آن سخت و خطاها تا حد ممکن در زمان کامپایل مشخص شوند.

use std::sync::Mutex;

let counter = Mutex::new(0);

در این حالت، داده درون Mutex محافظت می‌شود و هر دسترسی باید از طریق قفل انجام شود.

کار با Mutex و مفهوم Lock

برای تغییر داده داخل Mutex، باید آن را lock کنیم. متد lock یک Guard برمی‌گرداند که وقتی از Scope خارج شود، قفل به‌صورت خودکار آزاد می‌شود. این طراحی مبتنی بر RAII باعث می‌شود حتی در صورت Panic یا خطا، قفل به‌درستی آزاد شود و Deadlock ایجاد نشود.

let mut num = counter.lock().unwrap();
*num += 1;

در این مثال، تا زمانی که num در Scope وجود دارد، هیچ Thread دیگری نمی‌تواند به داده دسترسی داشته باشد.

ترکیب Thread و Mutex با Arc

Mutex به‌تنهایی برای Concurrency کافی نیست، چون Ownership یکتا دارد. برای اشتراک داده بین Threadها، باید از Arc یا Atomic Reference Counted استفاده کنیم. Arc نسخه‌ی Thread-safe از Rc است و به Rust اجازه می‌دهد داده را بین چند Thread به‌صورت ایمن به اشتراک بگذاریم. ترکیب Arc و Mutex یکی از رایج‌ترین الگوهای Concurrency در Rust است.

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());

در این مثال، چند Thread به‌صورت همزمان مقدار مشترک را تغییر می‌دهند اما Rust تضمین می‌کند هیچ Data Race رخ ندهد.

چرا Rust در Concurrency قابل اعتماد است؟

Rust بسیاری از مشکلات رایج Concurrency را به خطای کامپایل تبدیل می‌کند. اگر داده‌ای Thread-safe نباشد، Rust اجازه‌ی اشتراک آن را نمی‌دهد. Traitهایی مثل Send و Sync نقش کلیدی در این سیستم دارند و کامپایلر به‌صورت خودکار بررسی می‌کند که آیا یک نوع داده می‌تواند بین Threadها منتقل یا به اشتراک گذاشته شود یا نه. این سطح از ایمنی باعث می‌شود بسیاری از باگ‌ها قبل از اجرا حذف شوند.

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

Concurrency در Rust نه‌تنها قدرتمند بلکه به‌شکل بی‌سابقه‌ای ایمن است. Thread، Mutex و Arc ابزارهایی هستند که به شما اجازه می‌دهند برنامه‌های همزمان، سریع و قابل اعتماد بسازید بدون اینکه وارد جهنم Race Condition شوید. درک عمیق این مفاهیم، پایه‌ی ورود به مباحث پیشرفته‌تری مثل Async Programming و سیستم‌های High Performance در Rust است. اگر این مرحله را خوب یاد بگیرید، Rust عملاً یکی از امن‌ترین گزینه‌ها برای توسعه‌ی نرم‌افزارهای همزمان خواهد بود.

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

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

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

سه × یک =

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