دستور الملكية
Rust

من fullstack تعرف مشكلة C10K وحلّ Node لها: خيط واحد، event loop، عدم حجب أبدًا. تلك إجابة مشروعة — لكنها تتجنب التزامن الحقيقي بدل حلّ مشاكله. هذا الإقليم يعطيك الإجابة الأخرى: خيوط نظام تشغيل حقيقية، تنفيذ متوازٍ فعليًا على أنوية متعددة — لكن بلا الكابوس الذي يعرفه كل من كتب خيوطًا في C. والمفاجأة: البديهيتان اللتان بنيتَهما من الإقليم ٠١ لذاكرة خيط واحد فقط — ستكتشف الآن أنهما كانتا، طوال الوقت، تحلّان مشكلة أكبر بكثير.


الدرس ١: جرّب، قبل أن تُقال لك القاعدة

اللغز التأسيسي

في C، خيطان يكتبان لنفس المتغيّر بلا قفل = سباق بيانات (data race): undefined behavior، لا حتمية، قد يعمل ألف مرة وينهار في المرة الألف وواحد، على جهازك فقط، وقت العرض أمام الأستاذ. هذا الصنف من الأخطاء — لا تكرار، لا اختبار موثوق، لا سبب واضح — من أسوأ ما قد تصادفه في أي لغة نظام.

قبل أن تقرأ القاعدة، جرّب فعليًا:

use std::thread;

fn main() {
    let mut data = vec![1, 2, 3];
    let h1 = thread::spawn(move || { data.push(4); });
    let h2 = thread::spawn(move || { data.push(5); });  // ماذا يقول المترجم؟
    h1.join().unwrap();
    h2.join().unwrap();
}

اقرأ الرسالة كاملة. أي إقليم سبق أن رأيتَ فيه بالضبط هذا الرمز الخطئي (E0382)؟ فكّر: move أجبرت الإغلاق الأول على تمليك data كاملة (الإقليم ٠٦: لماذا يحتاج خيط جديد move تحديدًا — سيعيش الخيط بعد نهاية main المحلية، فلا يجوز أن يستعير من إطارها). فإذا مَلَك الإغلاق الأول data، ماذا تبقّى للثاني؟ لا شيء يخص التزامن في هذه الرسالة إطلاقًا — هذا خطأ "نقل قيمة مُنقولة سلفًا" العادي من الإقليم ٠١، يظهر هنا بمحض الصدفة الهندسية.

والآن التجربة الأهم — توقّعك بعد الإقليم الماضي سيكون: "أعرف الحل! Rc<RefCell<Vec<i32>>>. جرّبه فعليًا:

use std::rc::Rc;
use std::cell::RefCell;
let data = Rc::new(RefCell::new(vec![1, 2, 3]));
let d1 = Rc::clone(&data);
let h1 = thread::spawn(move || { d1.borrow_mut().push(4); });  // ارفض! اقرأ لماذا

المترجم يرفض بشدة أكبر هذه المرة: Rc<RefCell<...>> لا تطبّق Send. لماذا صمّم أهل اللغة Rc هكذا عمدًا؟ لأن عدّاد Rc عملية عادية غير ذرّية (count += 1، تُنفَّذ فعليًا كثلاث خطوات معالج: اقرأ، اجمع، اكتب). لو زادها خيطان في نفس اللحظة تمامًا، قد يقرآن نفس القيمة القديمة معًا، فيكتب كلاهما نفس النتيجة، وتُفقَد إحدى الزيادتين — سباق بيانات على العدّاد نفسه، حتى قبل أن تلمس Vec الداخلية. هذا بالضبط التحذير الذي تركناه معلَّقًا في نهاية الإقليم الماضي.

القاعدة التي كانت موجودة طوال الوقت

توقف وتأمّل ما حدث للتو: لم تحتج اللغة قاعدة جديدة واحدة للتزامن. البديهية الأولى (مالك واحد) رفضت السباق الأول. تصميم Rc (غير ذرّي عمدًا، لأن الذرّية لها ثمن أداء لا مبرر لدفعه في سياق خيط واحد) رفض الثاني. هذه هي "الصدفة": قواعد صُمِّمت بالكامل لحل مشاكل ذاكرة داخل خيط واحد — تبيّن أن سباق البيانات بين خيطين هو، من منظور نوع القيمة، نفس المشكلة تمامًا: أكثر من "مالك" فعّال لنفس الذاكرة، أحدهم يكتب. البديهيتان لا تعرفان الفرق بين "خيطين" و"نطاقين متداخلين في نفس الخيط" — وهذا هو سرّ المتانة.


الدرس ٢: Send وSync — الاسم الرسمي لما رأيتَه للتو

pub unsafe auto trait Send {}   // (تقريب توضيحي لتعريفها الفعلي)
pub unsafe auto trait Sync {}

هاتان سمتان (traits) بلا أي طريقة — علامتان محضتان:

الكلمة auto تعني: المترجم يشتقّهما بنيويًا بلا derive صريح — أي نوع كل حقوله Send هو Send تلقائيًا؛ وينسحب العكس أيضًا: نوع فيه حقل واحد ليس Send (كعدّاد Rc غير الذرّي) يصير تلقائيًا غير Send، بلا أي إعلان سلبي صريح مطلوب منك. هذا فرق عن #[derive(...)] من الإقليم ٠٤ (ذاك توليد كود تطلبه صراحةً؛ هذا استنتاج بنيوي يحدث سواء طلبتَه أو لا). تقريبًا كل نوع "عادي" Send + Sync تلقائيًا؛ Rc استثناء متعمَّد (non-auto عبر آلية داخلية تعطّل الاستنتاج له تحديدًا)، ومؤشرات خام (سترى في ٠٩) استثناء آخر.

thread::spawn يشترط إغلاقًا 'static + Send — الآن تقرأ هذا التوقيع بمعنى كامل: 'static لأن الخيط قد يعيش أطول من مُستدعيه (الإقليم ٠٥)، Send لأن ملكية كل ما التُقِط ستُنقَل فعليًا لخيط آخر.


الدرس ٣: Arc<T> وMutex<T> — التوءمان الذرّيان لـ Rc وRefCell

الحل ليس نوعًا جديد المفهوم — إنه نفس نمط الإقليم ٠٧، بنسخة مصمَّمة لتحمل تعديلًا متزامنًا فعليًا بأمان:

خيط واحد (الإقليم ٠٧)عدة خيوط (هنا)الفرق الجوهري
Rc<T>Arc<T>العدّاد ذرّي (Atomic — عملية معالج واحدة غير قابلة للتقسيم، لا نافذة زمنية يتداخل فيها خيطان)
RefCell<T>Mutex<T>الحصرية تُفرَض بقفل نظام تشغيل حقيقي (يُنيّم الخيط المنافس بدل panic فوري)
Rc<RefCell<T>>Arc<Mutex<T>>نفس التركيب، بضمانات تصمد أمام تشغيل حقيقي متوازٍ
use std::sync::{Arc, Mutex};
use std::thread;

let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let mut handles = vec![];
for i in 4..=5 {
    let d = Arc::clone(&data);
    handles.push(thread::spawn(move || {
        d.lock().unwrap().push(i);   // .lock() تُرجع Result — Mutex قد "يتسمم"
    }));
}
for h in handles { h.join().unwrap(); }
println!("{:?}", *data.lock().unwrap());

.lock() تُرجع Result<MutexGuard<T>, PoisonError<...>> — Result من الإقليم ٠٣ مجددًا. لماذا قد يفشل قفل؟ التسميم (poisoning): لو انهار (panic) خيط وهو حامل للقفل، يُعلَّم الـ Mutex "مسموم" — أي خيط لاحق يحاول قفله يستلم Err بدل الانتظار الأبدي على بيانات ربما تُركت في منتصف تحديث غير مكتمل. هذا ميزة أمان، لا عيب: صمت مثل هذا الفشل كان ليعني بيانات تالفة منطقيًا تُقرأ بثقة زائفة.


الدرس ٤: القنوات — "لا تتشارك الذاكرة للتواصل؛ تواصل لتتشارك الذاكرة"

بديل معماري لا يشارك ذاكرة أصلًا — يُرسل قيمًا مملوكة بالكامل بين الخيوط:

use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    tx.send(String::from("hello from thread")).unwrap();
});
let msg = rx.recv().unwrap();
println!("{msg}");

mpsc = multiple producer, single consumer (tx قابلة للاستنساخ بـ .clone() لمرسلين عدة؛ rx واحد فقط). tx.send(v) ينقل ملكية v فعليًا عبر القناة — بعد الإرسال، v ميتة في خيط المرسِل تمامًا كأي move عادي من الإقليم ٠١. لا يمكنك عمليًا أن تُسابق نفسك على قيمة أرسلتَها، لأن لا طريقة نحوية للوصول إليها بعد الإرسال. البديهية الأولى تحرس القنوات أيضًا، بلا أي كود إضافي خاص بالتزامن.

اختر بين Arc<Mutex<T>> والقنوات بذوق مكتسب لا بقاعدة صارمة: حالة مشتركة يقرؤها ويكتبها الجميع بكثرة ← غالبًا Mutex أوضح؛ تدفّق بيانات باتجاه واحد بين مراحل معالجة (بايبلاين) ← القنوات أوضح نيّة وأصعب في كسرها بالخطأ.


الألغاز

لغز ٠٨-أ: عداد السباق المُصلَح ثلاث مرات. ابدأ بهذا (سيرفضه المترجم — هذا مقصود):

let counter = 0;
let mut handles = vec![];
for _ in 0..10 {
    handles.push(thread::spawn(move || { counter += 1; }));
}

أصلحه بثلاث طرق مستقلة، كل واحدة في برنامج منفصل: (١) Arc<Mutex<i32>>، (٢) std::sync::atomic::AtomicI32 مع fetch_add (ابحث توقيعها في وثائق stdrustup doc --std — بلا Mutex إطلاقًا: العدّاد الذرّي الخام الذي تُبنى Arc فوق نفس فكرته)، (٣) القنوات (كل خيط يُرسل 1 عبر tx، الخيط الرئيسي يستقبل ١٠ رسائل ويجمعها). بعد الثلاثة: اكتب فقرة تقارن وضوح النية وكمية الكود لكل حل.

لغز ٠٨-ب: عدّاد الكلمات المتوازي. وزّع نصًّا طويلًا (بضع مئات الأسطر — ولّده أو الصق نصًّا حقيقيًا) على N خيوط بالتساوي (كل خيط يستلم شريحة أسطر — استعارة 'static عبر Arc< String> أو تمليك أجزاء مقسَّمة سلفًا، قرارك). كل خيط يبني HashMap<String, usize> لتكرار الكلمات في نصيبه، ويُرسل النتيجة عبر قناة. الخيط الرئيسي يدمج كل الخرائط الفرعية في خريطة نهائية واحدة (اجمع القيم لنفس المفتاح). قيد: ممنوع أي Mutex أو حالة مشتركة مباشرة في هذا اللغز — رسائل فقط. قارن النتيجة بتشغيل نفس المنطق على خيط واحد للتحقق من التطابق.

لغز ٠٨-ج: بحث مُوازٍ (تكرار متباعد). عُد لأداة البحث النصي من الإقليم ٠٦. وسّعها لتقبل عدة ملفات كمعاملات، وابحث في كل ملف على خيط مستقل، حيث يجمع الخيط الرئيسي كل النتائج (عبر قناة أو عبر Vec<JoinHandle<...>> و.join() على كل واحد وتجميع القيم الراجعة) ويطبعها مرتّبة حسب اسم الملف (لا حسب ترتيب انتهاء الخيوط — فكّر لماذا هذا الترتيب غير مضمون افتراضيًا، وكيف تفرضه بعد التجميع). أعِد استعمال دوال البحث والتحليل التي بنيتَها هناك بلا تعديل جوهري — إن احتجتَ تعديلًا، فهذا مؤشر أن التوقيع الأصلي كان يفترض خيطًا واحدًا ضمنيًا؛ وثّق ما غيّرتَه ولماذا.


الخلاصة

بذرة الغموض

كل ما بنيتَه من الإقليم ٠٠ حتى هنا صمد بلا أي جهاز تشغيل خفي يراقبك (لا GC، لا مفسّر). الإقليمان الأخيران يكسران هذا النمط مرتين، بطريقتين مختلفتين تمامًا: الأول يهبط بك تحت كل الضمانات التي بنيتَها لترى القلب غير الآمن الذي يقف تحتها جميعًا. الثاني يبني لغة كاملة ثانية فوق Rust نفسها لحل مشكلة عشرة آلاف الاتصال — لغة لم تكن Rust تملك حتى الكلمات المفتاحية اللازمة للتعبير عنها قبل أن تُضاف عمدًا.