عندما تتحكم الأحداث في الكود: Event-driven في Rust
\n\nتخيل أن الكود الخاص بك هو قائد أوركسترا. في البرمجة المتزامنة التقليدية، يقرر قائد الأوركسترا متى يجب أن تصدر كل نغمة. أما في معمارية الأحداث (Event-driven)، فإن قائد الأوركسترا يكتفي بالتفاعل مع ما يعزفه الموسيقيون بالفعل. يبدو متناقضًا؟ فقط للوهلة الأولى.
\n\nمعمارية الأحداث (Event-driven architecture) هي نهج يتم فيه تحديد تدفق تنفيذ البرنامج بواسطة الأحداث: نقرة ماوس، استلام طلب HTTP، اكتمال عملية إدخال/إخراج، مؤقت. Rust مع نظام الملكية (Ownership) وتجريدات التكلفة الصفرية هي أرض خصبة لمثل هذه المعمارية. خاصة إذا كنت قادمًا من Python، حيث يعمل event loop (مثل asyncio) ولكنه غالبًا ما يصطدم بـ GIL والتكاليف الإضافية للكتابة الديناميكية.
\n\nفي هذه المقالة، لن نتحدث عن النظرية فقط. سنكتب كودًا حقيقيًا، ونحلل معمارية الأحداث في Rust باستخدام الكريتات (crates) الشائعة، ونوضح كيف يمكنها تسريع تطبيقاتك أضعافًا مضاعفة.
\n\nلماذا Rust بدلاً من Python للأحداث؟
\n\nPython لغة رائعة للنماذج الأولية. لكن عندما تحتاج إلى معالجة الأحداث في الوقت الفعلي مع آلاف الاتصالات المتزامنة، يبدأ Python في التباطؤ. أما Rust فيمنحك:
\n\n- \n
- تجريدات بتكلفة صفرية — async/await لا يضيف أي حمل إضافي إذا لم تستخدم ميزاته \n
- عدم وجود جامع للقمامة (Garbage Collector) — لا توقف مفاجئ أثناء معالجة الأحداث \n
- نظام أنواع يكتشف الأخطاء في مرحلة الترجمة \n
- توازي حقيقي — ليس تعدد مهام تعاوني، بل خيوط حقيقية بدون GIL \n
إذا كنت تكتب تطبيقات تعتمد على الأحداث في Rust، فستحصل على تحكم كامل في كل بايت من الذاكرة وكل دورة معالجة. وهذا ليس تسويقًا — إنها حقيقة.
\n\nالأساسيات: Event Loop ومعالجة الأحداث
\n\nفي قلب أي نظام يعتمد على الأحداث يوجد event loop (حلقة الأحداث). وهي حلقة لا نهائية تقوم بـ:
\n\n- \n
- انتظار ظهور حدث جديد \n
- استخراجه من قائمة الانتظار \n
- تمريره إلى المعالج \n
- التكرار \n
في Rust، يتم توفير event loop القياسي بواسطة كريت tokio. لنكتب مثالًا بسيطًا:
use tokio::sync::mpsc;\nuse tokio::time::{sleep, Duration};\n\n#[tokio::main]\nasync fn main() {\n // إنشاء قناة لنقل الأحداث\n let (tx, mut rx) = mpsc::channel::<String>(32);\n\n // المنتج: يولد الأحداث\n tokio::spawn(async move {\n for i in 0..5 {\n let event = format!("الحدث رقم {}", i);\n tx.send(event).await.unwrap();\n sleep(Duration::from_millis(500)).await;\n }\n });\n\n // المستهلك: يعالج الأحداث\n while let Some(event) = rx.recv().await {\n println!("تمت المعالجة: {}", event);\n // هنا يمكن أن يكون منطق عملك\n }\n}\n\nيوضح هذا الكود النمط الأساسي لـ برمجة الأحداث في Rust: مكون يولد الأحداث، وآخر يعالجها. توفر القناة mpsc (multi-producer, single-consumer) نقلًا آمنًا للبيانات بين المهام.
مثال متقدم: نظام معالجة الطلبات
\n\nلنعقد المهمة. تخيل أننا نكتب الواجهة الخلفية لمتجر إلكتروني. عندما يصل طلب جديد، يجب:
\n\n- \n
- التحقق من توفر المنتج \n
- حجزه \n
- إرسال إشعار \n
- تحديث الإحصائيات \n
النهج التقليدي هو القيام بكل هذا بشكل متزامن. أما نهج Event-driven فهو تقسيم العمل إلى أحداث:
\n\nuse tokio::sync::broadcast;\nuse tokio::time::sleep;\nuse std::time::Duration;\n\n#[derive(Debug, Clone)]\nenum OrderEvent {\n NewOrder { id: u32, items: Vec<String>