مقدمة
نتعامل في لغات البرمجة مع أحداث تأخذ وقت وفي هذا الوقت لا يكون المعالج يعمل شيء. مثلا لما نقرأ من ملف أو نكتب في ملف أو لما نرسل استعلام لخادوم قاعدة بيانات بعيد وننتظر الرد أو لما نرسل طلب إلى واجهة برمجية بعيدة API وفي هذه الأثناء بعد إرسال الاستعلام أو الطلب ننتظر الرد. وبدل أن تكون الخدمة معطلة تنتظر الرد يمكنها استغلال الوقت الضائع في الإنتظار في معالجة طلبات أخرى. قديما كان النموذج الوحيد للتعامل مع أكثر من عملية في نفس الوقت هو تعدد العمليات وتعدد خيوط المعالجة (multi-process و multi-thread) سواء كل طلب جديد في عملية أو مسار جديد منفصل أو بعمل pool مجهز مسبقا وتحويل الطلبات عليه عبر طابور queue. لكن هذين النموذجين لا يناسبان هذه الحالة التي شرحناها لأن عنق الزجاج هنا ليس المعالج نحن في هذه الأمثلة لا ننتظر حسابات معينة فنريد نواة معالج أخرى تحسب بينما الأولى مشغولة في الحسابات الأولى. نحن فعليا ننتظر. ننتظر IO. سواء كان disk io أو network io. ومن هنا ظهرت عائلة من اللغات أو من العبارات الخاصة في اللغات التي تمكنك من معالجة أكثل من طلب ضمن عملية واحدة دون الحاجة لتعدد العمليات وطبعا الشرط الوحيد هنا أن لا تكون هذه الطلبات عبارة عن حسابات cpu intensive بل تكون io intensive هذه العائلة تسمى asyncio أو event driven.
وأيضا نتعامل في مع استعلامات لقواعد بيانات تعيد عدد كبير من النتائج. لنفرض أن لدي قاعدة بيانات فيها سجلات مليون طالب وأريد جلبها وعمل شيء معين عليها وليس من المنطق وضع المليون سجل في الذاكرة رام كلهم دفعة واحدة. توفر قواعد البيانات شيء يسمى cursor وتوفر لغات البرمجيات أدوات للتعامل مع تدفق سيالات البيانات التي تأتي تباعا تسمى generators. أو لنفرض أن لدي ملف نصي به مليون سطر وأريد أقصر أو أطول سطر في الملف. لا داع لرفع الملف كله في الذاكرة رام. كل ما يلزمني هو سطر واحد وكلما انتقلت للسطر التالي نسيت الذي قبله.
مع الأسف الكثير من المبرمجين لا يحسنون التعامل هذه المفاهيم وكثيرا ما يضعون الكثير من البيانات في الذاكرة رام أو يضيعون الموارد بحجز عدد كبير جدا من العمليات processes لم يكن لها داع أو مبرر. لذا صممت تمرين أوضح في هذه المفاهيم.
التمرين
من فترة نشرت "تمرين" بلغة جافاسكربت أو asyncio في بايثون 3. لدينا متوالية معلينة ولنقل متوالية فبوناتشي التي تبدأ ب 1 ثم 1 ثم مجموع آخر رقمين وهي لا تنتهي. هكذا 1 ثم 1 ثم 2 ثم 3 ثم 5 ثم 8 ثم 13 ...إلخ نريد دالة تعيد أرقام متسلسلة فبوناتشي واحدا واحدا لكنه ينتظر 10 ميليثانية بين الرقم والرقم اللي بعده (في محاكاة لكونه يرجع من استدعاء بعيد API call). ونريد عمل دالة تعيدها في صورة حزم bulks من حجم معين مثلا حجمها 10 عناصر. يعني تعيد أول 10 عناصر ثم ثاني 10 عناصر في المتسلسلة ثم اللي بعدهم ثم اللي بعدها ويجب أن تقبل أي متسلسلة نعطيها إياها (وليس بالضرورة فبوناتشي). ثم نريد عمل برنامج يطبع أول ثلاث حزم.
المطلوب تكون فبوناشي دالة منفصلة حتى نستبدلها بمتوالية أخرى مثلا تعيد 1 ثم 2 مكرر مرتين ثم 3 مكرر 3 مرات ثم 4 مكرر 4 مرات هكذا 1, 2, 2 , 3,3,3 , 4,4,4,4 ... وطبعا نريد 10 ميلي-ثانية بين كل رقمين.
جميع المحددات هنا مثل ال 10 ميلي-ثانية وال 10 عناصر وال 3 حزم يجب أن تكون متغيرات يمكن تمريرها sleep_ms و bulk_size و iteration_count
الحل المطلوب يجب أن لا يبتلع الرام ولا يتسبب في فيضان stack overflow
الهدف من التمرين
تم تصميم التمرين بحيث تكون المتوالية لا نهائية فمن تعود أن يضعها كلها في الرام لا ينجح. وتم إضافة غفوة 10 ميلي-ثانية بين كل رقمين لتحاكي إرسال طلب وانتظار نتيجته ثم عمل شيء عليه مثل تحديث الواجهة. مثل طلبات AJAX في الواجهة الأمامية أو مثل استعلامات قواعد البيانات في الواجهة الخلفية (يعني الموضوع هذا يهم مطوري ال frontend و ال backend على السواء).
بعض المشاكل الشائعة
تسلسل التنفيذ
- يقوم بعملية تأخذ وقت (اطلب مورد ما لا-تزامني مثلا طلب AJAX أو رفع أو تنزيل صورة)
- يعمل شيء على هذا المورد قبل أن يجهز فيحصل خطأ
var user=null; $.ajax({ dataType: "json", url: "/user/123", success: function(data) { user=data.user; }, }); console.log(user.name);
الفيض overflow
function fibonacci(n) { if (n<=2) return 1; return fibonacci(n-1)+fibonacci(n-2);
}
أحد الحلول لتجنب ذلك هي التذكر memoization بمعنى أن الدالة الأولى تحسب معها كل ما قبلها وتتذكره فلما نأتي للطرف الآخر في عملية الجمع تكون النتيجة محسوبة مسبقا. لكن هذا الحل غير عملي لأنها متوالية لا نهائية وأيضا هذه الدالة لا تحتاج إلا آخر رقمين كي تجمعمها يعني لما توصل لل 100 يلزمك 99 و 98 ولما تطلع إلى 101 يلزمك 100 و 99 ولا يلزمك تذكر الأرقام من 1 إلى 98 كلها مساحة مهدورة لا داع لتذكرها. الفرق جوهري وكبير لأن الاحتفاظ برقمين يعتبر ال O هو 1 في ال space complexity أما الاحتفاظ بكل ما سبق يعتبر O هو n وشتان بينهما.
مفاهيم مرتبطة بالحل
المولدات Generators
> let a=1; > let b=1; > [a, b] = [b, a+b]; [ 1, 2 ] > [a, b] = [b, a+b]; [ 2, 3 ] > [a, b] = [b, a+b]; [ 3, 5 ] > [a, b] = [b, a+b]; [ 5, 8 ] > [a, b] = [b, a+b]; [ 8, 13 ]
> function *fibonacci() { let a=1; let b=1; yield a; yield b; while(true) { [a, b] = [b, a+b]; yield b; } } > let gen=fibonacci(); > gen.next() { value: 1, done: false } > gen.next() { value: 1, done: false } > gen.next() { value: 2, done: false } > gen.next() { value: 3, done: false } > gen.next() { value: 5, done: false } > gen.next() { value: 8, done: false } > gen.next() { value: 13, done: false } > gen.next() { value: 21, done: false } > gen.next() { value: 34, done: false }
def fibonacci(): a=1 b=1 yield a yield b while(True): a, b = b, a+b yield b # interactive shell >>> gen=fibonacci() >>> next(gen) 1 >>> next(gen) 1 >>> next(gen) 2 >>> next(gen) 3 >>> next(gen) 5 >>> next(gen) 8 >>> next(gen) 13 >>> next(gen) 21 >>> next(gen) 34
الدوال اللامتزامنة async function
async function main() { let user = null; user = await get_ajax_user_promise(123); console.log(user.name); } main();
setTimeout(function() { do_something1(); setTimeout(function() { do_something2(); setTimeout(function() { do_something3(); }, ms); }, ms); }, ms);
function sleep(ms) { return new Promise(function(resolve){ setTimeout(resolve, ms); }); } function do_something(a) { console.log("got ", a); } (async function() { let ms=100; await sleep(ms); do_something(1); await sleep(ms); do_something(2); await sleep(ms); do_something(3); })();
#! /usr/bin/python3 import asyncio def do_something(a): print("got", a) async def main(): ms = 0.01 await asyncio.sleep(ms) do_something(1); await asyncio.sleep(ms) do_something(2); await asyncio.sleep(ms) do_something(3); if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main())
ليست هناك تعليقات:
إرسال تعليق