مقدمة: لماذا يُعتبر السينجلتون العالمي في Rust تحديًا؟
\n\nإذا كنت قادمًا من عالم Python أو JavaScript أو C++، فأنت معتاد على كائن عالمي بسيط. تعلن عن متغير على مستوى الوحدة النمطية - ويكون ملكك. في Rust، هذه الخدعة لن تنجح. يراقب المترجم بدقة عمر البيانات والوصول إليها، خاصة إذا كانت قابلة للتغيير من خيوط مختلفة. لهذا السبب بالتحديد، جمع سؤال Stack Overflow «How do I create a global, mutable singleton?» آلاف المشاهدات وعشرات الإجابات المعقدة.
\n\nلكن لا تتعجل في الخوف. السينجلتون في Rust ليس ألمًا، بل انضباط. اليوم سنشرح كيفية إنشاء سينجلتون عالمي قابل للتغيير بأمان وكفاءة مع فهم ما يحدث. ستتعرف على لماذا static mut هو شر، وما هي الأدوات (lazy_static، OnceCell، Mutex) التي تمنحنا السيطرة.
المقالة مكتوبة للموقع التعليمي pythonlib.ru وتكييف سؤال حقيقي من SO إلى درس عملي. هيا بنا!
\n\nالمشكلة: ما هو السينجلتون العالمي القابل للتغيير؟
\n\nالسينجلتون هو نمط يكون فيه للفئة أو الهيكل نسخة واحدة بالضبط في جميع أنحاء البرنامج. في سياق Rust، نحن نتحدث عن متغير عالمي يمكن الوصول إليه من أي مكان في الكود، ويمكن تغييره (mut).
\n\nأمثلة على الاستخدام:
\n- \n
- تكوين التطبيق، الذي يتم تحميله مرة واحدة عند بدء التشغيل. \n
- مسجل عالمي أو عداد أحداث. \n
- مجموعة اتصالات بقاعدة البيانات. \n
- ذاكرة تخزين مؤقت في الذاكرة (in-memory cache). \n
المشكلة هي أن Rust يمنع المتغيرات العالمية القابلة للتغيير بدون غلاف خاص. لماذا؟ لأنها تؤدي إلى سباقات البيانات (data races) وسلوك غير محدد. لا يستطيع المترجم إثبات أن خيطين لن يكتبا في نفس خلية الذاكرة في نفس الوقت.
\n\nالحل الساذج يبدو هكذا:
\n// كود سَيء - لا تفعل هذا!\nstatic mut COUNTER: u32 = 0;\n\nfn increment() {\n unsafe {\n COUNTER += 1;\n }\n}\n\nهذا يعمل، لكنه يتطلب unsafe ويتركك وحدك مع سباقات البيانات. في بيئة متعددة الخيوط - هذه قنبلة موقوتة. نحتاج إلى نهج آمن ومنسجم مع المصطلحات.
الأداة 1: lazy_static + Mutex - كلاسيكيات هذا النوع
\n\nالطريقة الأكثر شيوعًا لإنشاء سينجلتون عالمي قابل للتغيير هي الجمع بين lazy_static (التهيئة البطيئة) و Mutex (الاستبعاد المتبادل).
\n\nأضف التبعيات في Cargo.toml:
[dependencies]\nlazy_static = "1.4"\n\n\nالآن الكود:
\nuse lazy_static::lazy_static;\nuse std::sync::Mutex;\n\nlazy_static! {\n static ref GLOBAL_CONFIG: Mutex<Config> = Mutex::new(Config::default());\n}\n\n#[derive(Debug)]\nstruct Config {\n db_url: String,\n max_connections: u32,\n}\n\nimpl Default for Config {\n fn default() -> Self {\n Config {\n db_url: "localhost:5432".to_string(),\n max_connections: 10,\n }\n }\n}\n\nfn main() {\n // قراءة\n let config = GLOBAL_CONFIG.lock().unwrap();\n println!("الرابط الحالي: {}", config.db_url);\n\n // كتابة\n // drop(config); // تحرير القفل صراحةً\n let mut config = GLOBAL_CONFIG.lock().unwrap();\n config.db_url = "new-host:8888".to_string();\n println!("الرابط الجديد: {}", config.db_url);\n}\n\n\nكيف يعمل هذا؟
\n- \n
lazy_static!يضمن أن التهيئة ستحدث مرة واحدة فقط عند أول وصول. \n Mutex<T>يوفر الاستبعاد المتبادل: يمكن لخيط واحد فقط الوصول إلى البيانات في أي لحظة. \n - الطريقة
.lock()ترجعMutexGuard، الذي يحرر القفل تلقائيًا عند الخروج من النطاق. \n
الإيجابيات: البساطة، مجربة عبر الزمن، آمنة لـ