
روز ۴۳: درک عمیق Send و Sync در Rust و نقش آنها در ایمنی همزمانی
یکی از تفاوتهای بنیادین Rust با سایر زبانها در حوزهی Concurrency، وجود Traitهایی است که مستقیماً به ایمنی همزمانی مربوط میشوند. Send و Sync دو Trait بسیار مهم و در عین حال نامرئی هستند که معمولاً مستقیماً آنها را پیادهسازی نمیکنیم، اما تقریباً تمام سیستم همزمانی Rust بر پایهی آنها ساخته شده است. درک درست Send و Sync نقطهای است که شما را از یک Rust Developer معمولی به کسی تبدیل میکند که واقعاً فلسفهی زبان را فهمیده است.
Send چیست و چه مشکلی را حل میکند؟
Trait به نام Send مشخص میکند که آیا مالکیت یک مقدار میتواند به Thread دیگری منتقل شود یا نه. اگر یک نوع داده Send باشد، به این معنی است که انتقال Ownership آن بین Threadها امن است و باعث بروز Data Race یا رفتار تعریفنشده نمیشود. در Rust، اکثر انواع ساده مثل i32، String، Vec<T> و Structهایی که فقط از انواع Send ساخته شدهاند، بهصورت پیشفرض Send هستند. این Trait تضمین میکند که وقتی دادهای به Thread جدید منتقل میشود، Thread قبلی دیگر به آن دسترسی ندارد.
در عمل، Send همان چیزی است که اجازه میدهد از move در thread::spawn استفاده کنیم. اگر نوعی Send نباشد، کامپایلر Rust اجازه نمیدهد آن داده وارد Thread جدید شود. این تصمیم در زمان کامپایل گرفته میشود و جلوی طیف بزرگی از خطاهای همزمانی را میگیرد.
use std::thread;
let value = String::from("Hello Rust");
thread::spawn(move || {
println!("{}", value);
});
در این مثال، String نوعی Send است و انتقال Ownership آن به Thread جدید کاملاً امن تلقی میشود.
Sync چیست و چه تفاوتی با Send دارد؟
Trait Sync نشان میدهد که آیا یک نوع داده میتواند بهصورت ایمن بین چند Thread بهصورت Reference مشترک استفاده شود یا نه. اگر نوعی Sync باشد، به این معنی است که &T میتواند همزمان در چند Thread وجود داشته باشد بدون اینکه Data Race ایجاد شود. تفاوت مهم Sync با Send این است که Sync دربارهی اشتراک Reference صحبت میکند، نه انتقال Ownership.
به زبان ساده، Send یعنی «میتوانم این داده را به Thread دیگر بدهم»، اما Sync یعنی «میتوانم چند Thread همزمان به این داده نگاه کنند». این تفاوت کوچک اما بسیار حیاتی است، چون بسیاری از مشکلات Concurrency دقیقاً در اشتراک همزمان داده رخ میدهند.
رابطهی Send و Sync با Mutex و Arc
در درس قبل دیدیم که چگونه Arc و Mutex کنار هم استفاده میشوند. حالا دلیل واقعی آن را میفهمیم. Mutex<T> زمانی Sync است که T خودش Send باشد. این یعنی دادهی داخل Mutex میتواند بهصورت ایمن بین Threadها محافظت شود. Arc نیز Send و Sync است، به همین دلیل میتواند Reference Counting ایمن برای Threadها فراهم کند.
use std::sync::{Arc, Mutex};
use std::thread;
let data = Arc::new(Mutex::new(5));
let data_clone = Arc::clone(&data);
thread::spawn(move || {
let mut num = data_clone.lock().unwrap();
*num += 1;
});در این مثال، Arc مسئول اشتراک Ownership و Mutex مسئول جلوگیری از دسترسی همزمان خطرناک است. Send و Sync در پشت صحنه تضمین میکنند این ترکیب امن است.
چرا Rc Send و Sync نیست؟
یکی از مثالهای کلاسیک برای درک Send و Sync، مقایسهی Rc و Arc است. Rc برای محیطهای تکThread طراحی شده و از Reference Counting غیراتمیک استفاده میکند. به همین دلیل Rc نه Send است و نه Sync. اگر Rust اجازه میداد Rc بین Threadها استفاده شود، کوچکترین افزایش یا کاهش شمارنده میتوانست باعث Race Condition شود.
use std::rc::Rc;
use std::thread;
let value = Rc::new(5);
thread::spawn(move || {
println!("{}", value);
});این کد کامپایل نمیشود و دقیقاً به همین دلیل Rust را زبان ایمن میدانند. این خطا پیش از اجرا جلوی یک باگ بسیار خطرناک را میگیرد.
پیادهسازی دستی Send و Sync و خطرات آن
در شرایط بسیار خاص، میتوان Send و Sync را بهصورت unsafe برای یک نوع پیادهسازی کرد. این کار فقط زمانی مجاز است که شما ۱۰۰٪ مطمئن باشید نوع دادهی شما در محیط همزمان ایمن است. Rust این امکان را فراهم کرده، اما بهشدت توصیه میکند تنها توسعهدهندگان بسیار باتجربه سراغ آن بروند.
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}این کد به کامپایلر میگوید «به من اعتماد کن»، و اگر اشتباه کنید، تمام تضمینهای ایمنی Rust از بین میرود. به همین دلیل در پروژههای واقعی بسیار نادر است که نیاز به پیادهسازی دستی این Traitها باشد.
Send و Sync چگونه Rust را متفاوت میکنند؟
در بسیاری از زبانها، Concurrency یک ویژگی اضافهشده روی زبان است. در Rust، Concurrency بخشی از Type System است. Send و Sync باعث میشوند بسیاری از تصمیمات مهم دربارهی همزمانی، نه در Runtime بلکه در Compile Time گرفته شوند. این یعنی برنامهای که کامپایل میشود، از نظر دسترسی همزمان به حافظه تا حد زیادی تضمینشده است.
این رویکرد باعث میشود Rust برای سیستمهای High Performance، Serverهای همزمان، موتورهای بازی و نرمافزارهای سطح پایین انتخابی فوقالعاده باشد.
جمعبندی نهایی
Send و Sync ستونهای پنهان Concurrency در Rust هستند. بدون آنها، Thread، Mutex و Arc معنا نداشتند. Send مشخص میکند چه دادهای میتواند بین Threadها منتقل شود و Sync تضمین میکند چه دادهای میتواند بهصورت مشترک استفاده شود. درک این دو Trait به شما کمک میکند خطاهای همزمانی را قبل از اجرا حذف کنید و با اطمینان سیستمهای پیچیده و چندنخی طراحی کنید. اگر این درس را خوب بفهمید، Rust دیگر برایتان ترسناک نخواهد بود، بلکه منطقی و قابل پیشبینی میشود.