مقدمه

تاکنون دیده‌ایم که چگونه سیستم مالکیت و قوانین وام‌گیری Rust به نوشتن کدهای همزمان ایمن کمک می‌کنند. اما چگونه کامپایلر این تضمین‌ها را اعمال می‌کند؟ پاسخ در دو trait نشانگر (marker trait) به نام‌های Send و Sync نهفته است. این traitها هیچ متدی ندارند؛ تنها وجود آنها به کامپایلر اطلاعاتی درباره ویژگی‌های همزمانی یک نوع می‌دهد و به آن اجازه می‌دهد تا در زمان کامپایل، ایمنی را تضمین کند.

Send Trait: انتقال مالکیت بین ترِدها

Send نشان می‌دهد که مالکیت یک مقدار از نوعی که trait را پیاده‌سازی کرده، می‌تواند به صورت ایمن بین ترِدهای مختلف منتقل شود. به عبارت دیگر، اگر نوع T، Send باشد، شما می‌توانید یک مقدار از نوع T را به یک ترِد دیگر move کنید.

تقریباً تمام نوع‌های اولیه در Rust (مانند i32، bool، String و Vec<T>) Send هستند، به شرطی که تمام اعضای داخلی آنها نیز Send باشند. تنها استثنای مهم، اشاره‌گر هوشمند Rc<T> است. Rc<T> برای مدیریت شمارش ارجاع خود از عملیات غیر اتمیک استفاده می‌کند و به همین دلیل اشتراک‌گذاری آن بین ترِدها ایمن نیست و Send را پیاده‌سازی نمی‌کند. به همین دلیل بود که در درس قبل برای اشتراک‌گذاری داده بین ترِدها، از Arc<T> استفاده کردیم که نسخه اتمیک و ایمن برای محیط‌های چند-ریسمانی است.

Sync Trait: دسترسی همزمان از چندین ترِد

Sync نشان می‌دهد که یک مقدار از نوعی که این trait را پیاده‌سازی کرده، می‌تواند به صورت ایمن توسط چندین ترِد به صورت همزمان به اشتراک گذاشته شود. به عبارت دیگر، اگر &T (یک رفرنس تغییرناپذیر به T) از نوع Send باشد، آنگاه خود T از نوع Sync است. این یعنی می‌توان یک رفرنس به آن را بدون مشکل به ترِدهای دیگر ارسال کرد.

مانند Send، بیشتر نوع‌های اولیه در Rust نیز Sync هستند. اما نوع‌هایی که به صورت داخلی از مکانیزم‌های غیر ایمن برای همزمانی استفاده می‌کنند، Sync نیستند. مهم‌ترین مثال در اینجا، RefCell<T> است. RefCell<T> شمارش ارجاع‌های خود را در زمان اجرا و به صورت غیر اتمیک مدیریت می‌کند. اگر چندین ترِد به صورت همزمان سعی در تغییر این شمارنده داشته باشند، یک race condition رخ خواهد داد. به همین دلیل، RefCell<T> نه Send است و نه Sync.

چگونه Mutex<T> ایمنی را فراهم می‌کند؟

حالا که با Sync و Send آشنا شدیم، می‌توانیم درک کنیم که چرا ترکیب Arc<Mutex<T>> برای اشتراک‌گذاری وضعیت بین ترِدها ایمن است.

نوع Mutex<T> به خودی خود Sync است. شاید این عجیب به نظر برسد، اما دلیل آن این است که خود Mutex برای مدیریت قفل داخلی‌اش از عملیات اتمیک استفاده می‌کند. بنابراین، به اشتراک گذاشتن خود Mutex بین ترِدها ایمن است. اما مهم‌تر از آن، Mutex<T> تنها زمانی Send و Sync است که نوع داخلی آن، یعنی T، از نوع Send باشد. این تضمین می‌کند که شما نمی‌توانید یک مقدار غیرایمن (مانند Rc<T>) را در داخل یک Mutex قرار داده و آن را بین ترِدها به اشتراک بگذارید.

پیاده‌سازی دستی Send و Sync

از آنجایی که Send و Sync به صورت خودکار برای نوع‌هایی که کاملاً از اعضای Send و Sync تشکیل شده‌اند، پیاده‌سازی می‌شوند، شما به ندرت نیاز به پیاده‌سازی دستی آنها خواهید داشت. انجام این کار به صورت دستی معمولاً تنها زمانی لازم است که با کدهای ناامن (unsafe) سروکار دارید و باید به صورت دستی به کامپایلر تضمین دهید که نوع شما برای استفاده در محیط‌های همزمان ایمن است. این یک مبحث پیشرفته است که نیازمند دقت بسیار بالایی است.

در این درس با traitهای نشانگر Send و Sync و نقش حیاتی آنها در مدل همزمانی بدون ترس Rust آشنا شدیم. این traitها به کامپایلر اجازه می‌دهند تا در زمان کامپایل، تضمین کند که تمام عملیات انتقال و اشتراک‌گذاری داده بین ترِدها به صورت ایمن انجام می‌شود. با این درس، فصل «برنامه‌نویسی غیرهمزمان» به پایان می‌رسد. ما اکنون درک عمیقی از ابزارهای قدرتمند Rust برای نوشتن برنامه‌های همزمان و موازی داریم. در فصل بعدی، به بررسی «ویژگی‌های شی‌گرایی در Rust» خواهیم پرداخت و خواهیم دید که چگونه Rust مفاهیم شی‌گرایی را به روش منحصر به فرد خود پیاده‌سازی می‌کند.