التزامن القابل للتوسع باستخدام Send و Sync (Extensible Concurrency with Send and Sync)
من المثير للاهتمام أن كل ميزة من ميزات التزامن (concurrency) التي تحدثنا عنها حتى الآن في هذا الفصل كانت جزءاً من المكتبة القياسية (standard library)، وليست جزءاً من اللغة نفسها. خياراتك للتعامل مع التزامن لا تقتصر على اللغة أو المكتبة القياسية؛ يمكنك كتابة ميزات التزامن الخاصة بك أو استخدام تلك التي كتبها الآخرون.
ومع ذلك، من بين مفاهيم التزامن الرئيسية المضمنة في اللغة بدلاً من المكتبة القياسية هي سمات (traits) الواسمات (marker traits) الموجودة في std::marker وهما Send و Sync.
نقل الملكية بين الخيوط (Transferring Ownership Between Threads)
تشير سمة الواسم Send إلى أن ملكية (ownership) القيم من النوع الذي ينفذ Send يمكن نقلها بين الخيوط (threads). ينفذ كل نوع في Rust تقريباً سمة Send ، ولكن هناك بعض الاستثناءات، بما في ذلك Rc<T>: لا يمكن لهذا النوع تنفيذ Send لأنه إذا قمت بنسخ (cloned) قيمة Rc<T> وحاولت نقل ملكية النسخة إلى خيط آخر، فقد يقوم كلا الخيطين بتحديث عداد المراجع (reference count) في نفس الوقت. لهذا السبب، تم تنفيذ Rc<T> للاستخدام في حالات الخيط الواحد (single-threaded) حيث لا تريد دفع ضريبة أداء الأمان بين الخيوط.
لذلك، يضمن نظام الأنواع (type system) في Rust وقيود السمات (trait bounds) أنك لن ترسل أبداً قيمة Rc<T> عبر threads بشكل غير آمن عن طريق الخطأ. عندما حاولنا القيام بذلك في القائمة 16-14، حصلنا على الخطأ the trait `Send` is not implemented for `Rc<Mutex<i32>>`. عندما انتقلنا إلى Arc<T> ، الذي ينفذ Send ، تم تجميع (compiled) الكود.
أي نوع يتكون بالكامل من أنواع تنفذ Send يتم وسمه تلقائياً كـ Send أيضاً. جميع الأنواع الأولية (primitive types) هي Send تقريباً، باستثناء المؤشرات الخام (raw pointers)، والتي سنناقشها في الفصل العشرين.
الوصول من خيوط متعددة (Accessing from Multiple Threads)
تشير سمة الواسم Sync إلى أنه من الآمن للنوع الذي ينفذ Sync أن يتم الرجوع إليه من threads متعددة. بعبارة أخرى، أي نوع T ينفذ Sync إذا كان &T (مرجع غير قابل للتغيير لـ T) ينفذ Send ، مما يعني أنه يمكن إرسال المرجع (reference) بأمان إلى خيط آخر. على غرار Send ، تنفذ جميع primitive types سمة Sync ، والأنواع المكونة بالكامل من أنواع تنفذ Sync تنفذ أيضاً Sync.
المؤشر الذكي (smart pointer) من نوع Rc<T> لا ينفذ أيضاً Sync لنفس الأسباب التي تجعله لا ينفذ Send. النوع RefCell<T> (الذي تحدثنا عنه في الفصل الخامس عشر) وعائلة الأنواع المرتبطة Cell<T> لا تنفذ Sync. إن تنفيذ فحص الاستعارة (borrow checking) الذي يقوم به RefCell<T> في وقت التشغيل (runtime) ليس آمناً بين الخيوط. ينفذ smart pointer من نوع Mutex<T> سمة Sync ويمكن استخدامه لمشاركة الوصول مع threads متعددة، كما رأيت في قسم “الوصول المشترك إلى Mutex<T>”.
تنفيذ Send و Sync يدوياً غير آمن (Implementing Send and Sync Manually Is Unsafe)
بما أن الأنواع المكونة بالكامل من أنواع أخرى تنفذ سمات Send و Sync تنفذ أيضاً Send و Sync تلقائياً، فلا يتعين علينا تنفيذ تلك traits يدوياً. وبصفتها marker traits، فهي لا تحتوي حتى على أي دوال (methods) لتنفيذها. إنها مفيدة فقط لفرض الثوابت (invariants) المتعلقة بـ concurrency.
يتضمن تنفيذ هذه السمات يدوياً كتابة كود Rust غير آمن (unsafe Rust code). سنتحدث عن استخدام unsafe Rust code في الفصل العشرين؛ في الوقت الحالي، المعلومات المهمة هي أن بناء أنواع متزامنة جديدة لا تتكون من أجزاء تنفذ Send و Sync يتطلب تفكيراً دقيقاً للحفاظ على ضمانات الأمان. يحتوي كتاب “The Rustonomicon” على مزيد من المعلومات حول هذه الضمانات وكيفية الحفاظ عليها.
ملخص (Summary)
ليست هذه هي المرة الأخيرة التي سترى فيها التزامن في هذا الكتاب: الفصل القادم يركز على البرمجة غير المتزامنة (async programming)، وسوف يستخدم المشروع في الفصل الحادي والعشرين المفاهيم الواردة في هذا الفصل في موقف أكثر واقعية من الأمثلة الصغيرة التي نوقشت هنا.
كما ذكرنا سابقاً، ولأن القليل جداً من كيفية تعامل Rust مع concurrency هو جزء من اللغة، فإن العديد من حلول التزامن يتم تنفيذها كـ حزم (crates). هذه تتطور بسرعة أكبر من المكتبة القياسية، لذا تأكد من البحث عبر الإنترنت عن أحدث crates لاستخدامها في حالات الخيوط المتعددة (multithreaded).
توفر مكتبة Rust القياسية قنوات (channels) لتمرير الرسائل (message passing) وأنواع المؤشرات الذكية، مثل Mutex<T> و Arc<T> ، والتي تعد آمنة للاستخدام في سياقات التزامن. يضمن نظام الأنواع وفاحص الاستعارة (borrow checker) أن الكود الذي يستخدم هذه الحلول لن ينتهي به الأمر بسباقات البيانات (data races) أو مراجع غير صالحة. بمجرد أن يتم تجميع الكود الخاص بك، يمكنك أن تطمئن إلى أنه سيعمل بسعادة على threads متعددة دون أنواع الأخطاء التي يصعب تتبعها والشائعة في اللغات الأخرى. لم تعد البرمجة المتزامنة مفهوماً يدعو للخوف: انطلق واجعل برامجك متزامنة، بلا خوف!