السلام عليكم ورحمة الله وبركاته
في عالم رقمي سريع التطور بات مطلبه السرعة والكفاءة، ظهر نهج البرمجة المتزامنة، نهجٌ تحررنا به من أغلال البرمجة المتوالية synchronous، وفُتِحَ به عالم جديد من الإمكانيات، يُمكِّنُنَا من بناء أنظمة سريعة الاستجابة متعددة المهمات multitasking، فما هو التزامن وكيف نطبقه في بايثون؟
قبل أن أشرع في شرحه سأبدأ بثلاثة مفاهيم، التزامن والتوازي وتعدد المهمات، يُخلَطُ بينها حتى في العالم الغربي، لكن سأقدم قبلها مثالا عن التنفيذ التسلسلي:
التنفيذ التسلسلي

هب أن عندك برنامجًا يرسل طلبًا request إلى موقع ما على الشابكة internet، وهذا الطلب يتأخر، يستغرق س من الزمن، أو يستعلم query عن شيء في قاعدة المعطيات database مثل PostgreSQL، وهذا البرنامج مكتوب بأسلوب تسلسلي sequentially: الوظيفة أ تعمل حتى تنتهي، ثم تبدأ الوظيفة ب، وهكذا دواليك…
فهل تظن برنامجك سيكون سريعًا؟
هب أن س من الزمن يساوي ثانية، س = ١، وأنت تود تحميل مائة صورة، هذا سيستغرق ١٠٠ ثانية لينتهي!
أويتوقف ١٠٠ ثانية ليكمل باقي مهماته؟
ولأجل حل هذا البطء أتى مفهوم التزامن!
فبتطبيقه، نظريًا، سنحمل مئة صورة في ثانية واحدة!
لنشرع بشرح المفاهيم!
١. التزامن Concurrency
التزامن: تزامن الحدثان: وقعا في الوقت نفسه. أي أنه وقوع شيئين في الوقت نفسه.
إذا قلنا إن مهمتان تزامنتا، فنحن نقصد أنهما شرعتا في الوقت نفسه. وهذا أمر يحدث في حياتنا.
على سبيل المثال، في يوم من الأيام، فتح صاحبنا عبود مخبزًا، وكان يعد كعكتين، لنرى ما عبود فاعل في عمله!
فلخبز الكعكتين سيسخن عبود الفرن، وتسخينه يستغرق وقتًا قد يصل عشر دقائق، وفي أثناء هذا ليس لزامًا عليه أن ينتظره حتى يسخن، فعبود ليس بهينٍ، بل قد يؤدي مهمات أخرى، مثل خلط الدقيق والسكر والبيض معًا.
وأيضًا ليس لزامًا عليه أن ينتظر انتهاء الكعكة الأولى حتى يشرع في الثانية، بل قد يبدأ في خلط مكونات الكعكة الأولى في الخلاط، ويذهب ليُعِدَّ الكعكة الثانية حتى ينتهي الخلط.
في هذا المثال (تسخين الفرن وخلط المكونات أو خلط مكونات الكعكة الأولى والشروع في الثانية) بدل عبود بين مهمتين مختلفتين في وقت واحد، وهذا هو السلوك المتزامن، مهمات تُعالج في الوقت نفسه!
٢. التوازي Parallelism
كما سبق البيان، فإن التزامن مهمات متعددة تُعالج في الوقت نفسه، إلا أنهما لا يعملان معًا بالتوازي!
فقولنا إن شيئا ما يعمل بالتوازي فإننا لا نقصد أنه مهمتين تعملان بالتزامن، بل نقصد أننا نقصد أنهما يبدآن العمل في الوقت نفسه.
وبالعودة إلى مثال الخباز العبود والكعكتين، فعبود بعد ازدهار عمله ذاع صيته بين الأنام، وكَثُرَت طلبات الكعك وزاد عدد الزبائن وكان ما لا بد منه أن يستعين بمساعدٍ معه، اسمه عبد الرحمن، فأتى بهذا المساعد، وكان عبود يعد الكعكة الأولى، ومساعده عبد الرحمن يُعِدُّ الثانية، أي يعملان بالتوازي، يخلطان الخليط في الوقت نفسه وغيره،
كما ترى في الصورة الآتية:

وكما ترى في الصورة، في التزامن لدينا مهمات تعالج في الوقت نفسه، لكن في نقطة من الزمن نؤدي مهمة واحدة. أما في التوازي، فلدينا مهمات تُشرَع وتُعَالج بالتوازي، أي المهمة أ تعمل والوظيفة ب تعمل.
مثال على المفهومين
وبإسقط ما سبق على سياق البرمجيات التي يديرها نظام التشغيل = هب أن برنامجين يعملان في النظام المتزامن، فإنه سيبدل بين تشغيل هذين البرنامجين: سيشغل البرنامج الأول لمدة من الزمن قبل أن يشرع بالثاني.
وإذا فعلنا هذا بسرعة فإنه يظهر كأنه نظام يشغل البرنامجين في الوقت نفسه.
هب الآن أن برنامجين يعملان في النظام المتوازي، يُشَغِّل برنامجين في وقت واحد، فنحن نستعمل التزامن!
الأمر مربك إن لم تفهم، فقد يكون الخطأ من شرحي لا من فهمك، فعليك فهم سبب كون المفهومين مميزين.
الفرق بين التزامن والتوازي
باختصار غير مخل، فإن الفرق الجوهري بين التزامن والتوازي هو أن التزامن مهمة مستقلة عن الأخرى، ويمكن تطبيقه على معالج CPU بنواة واحدة، فإن المهمة ستستعمل شيئا يسمى تعدد مهمات تعاوني (تفصيله في الفقرة التالية) للتبديل بين المهمات، أي ستبدأ المهمة الأولى لمدة من الزمن ثم تذهب إلى الثانية…
أما التوازي فعلى النظام تنفيذ مهمتين أو أكثر في نفس الوقت، وعلى جهاز بمعالج ذو نواة واحدة فهذا غير ممكن، فإننا بحاجة إلى معالج متعدد الأنوية لتشغيل مهمتين معًا.
وكما ترى، قد يعني التوازي التزامن، لكن التزامن لا يعني التوازي، فالبرنامج متعدد المسالك multithreaded الذي يُشَغَّل على حاسوب متعدد الأنوية قد يكون متزامنًا ومتوازيًا. فإن هذا الحاسوب متعدد الأنوية، لِنَقُلْ ذو نواتين، قد يشغل أكثر من مهمة في الوقت نفسه (التزامن)، ونواتان تنفذان الرِّماز code بكل مهمة من المهمات (التوازي).
نسيت أخبرك ماذا حل بعبود وصاحبه عبد الرحمن!
في ليلة من الليالي، بعد اشتداد الجوع أقفلا المخبز وأكلا الأكل كله، وعزما أصحابهما ليتشاركا معهما السهرة، وقد أفلس عبود وعاد للبرمجة 🙂
.٣ تعدد المهمات Multitasking
وهذا المفهوم الأخير الذي نعرفه جميعنا، تعدد المهمات
تعدد المهمات في حياتنا اليومية كثير، وخاصة عند النساء 🙂
تطبخ وتتصل تحدث أمها أو صديقتها، أو قد يقرأ شخص ما كتابا وهو في الحافلة حتى يصل إلى بيته.
وسبب تأخر ذكري لهذه المفهوم على سهولته، أنه ينقسم إلى نوعين:
نوعي تعدد المهمات

١. تعدد مهمات شُفْعِيٌ preemptive multitasking
اسمه غريب عليك أيها العربي؟
من شُفْعَة، وشُفْعَة الدارِ أو الأرضِ: حق تفضيل أحد المشترين على غيره عند عرضها للبيع بشروط خاصة أو بعقد. تعريفه في الفقرة التالية…
٢. تعدد مهمات تعاوني cooperative multitasking
لنعرفهمها!
تعدد مهمات شُفْعِيٌ preemptive multitasking
تعدد مهمات شُفْعِيٌ: نوعٌ من أنواع تَعدّد المهمات، في هذا النوع يقاطِع نظامُ التشغيل دوريًا تنفيذ البرنامج ويمرر التحكّم في البرنامج إلى برنامجٍ آخر ينتظر.
يَمنعُ تعددُ المهمات الشفْعي أيَّ برنامجٍ من احتكار النظام، وأما كيفية عمل هذا النوع فهذا شيء لا يتسع له المقال، ويكفيك معرفته فحسب.
تعدد مهمات تعاوني cooperative multitasking
تعدد مهمات تعاوني: نوع من أنواع تعدد المهمات، في هذا النوع لا نعتمد على نظام التشغيل لتحديد متى يُبَدَّل بين المهمات التي تنفذ الآن، بل نحن نحدد أماكن في الرِّماز code للتبديل بين المهمات.
سُمِّي بالتعاوني لأن المهمات في برنامجنا تتعاون فيما بينها، فالمهمة أ تقول: سأتوقف مؤقتًا، لمدة من الزمن، وأنت أيها النظام امضِ قدمًا وشغل باقي المهمات (تعلمن أيتها النسوة 🙂 ).
ما الحاجة من هذين التعريفين يا مبعسس؟
لنعرف هذا في الفقرة التالية!
مكتبة asnycio في بايثون

أظنني قد أثقلت عليك أيها القارئ بمقالي هذا، وأبعدت بك الرحلة، ولكني لم أبعد بك في الحقيقة، لأني أردتك أن تفهم ما نحن بصدد فعله حتى لا تطبق بغير فهم، ولو خرجت من المقال بما قد قدمناه لكان خيرًا وبركة.
مكتبة تزامن الدخل أو الخرج asnycio مكتبة مستعملة في لغة البرمجة بايثون لتحقيق التزامن، تستعمل نوع تعدد مهمات تعاوني cooperative multitasking.
لما نصل في الرِّماز إلى نقطة ستستغرق وقتًا حتى تظهر نتيجة وعلينا الانتظار فإننا نستعمل مكتبة asnycio لنَنْقُلْ المهمة إلى الخلفية ونستكمل باقي المهمات حتى تنتهي المهمة من عملها.
بمجرد انتهاء المهمة فإن العملة تستأنف عملها (أي تكمله، الاستئناف: الاستكمال، وليس التوقف)، وهذا يُعَدُّ تزامنًا، لأننا سنبدأ مهمات كثيرة في نفس الوقت، ولكنها ليست بالتوازي.
السؤال الآن: كيف نفعلها؟
بحلقة الأحداث!
حلقة الأحداث event loop

حلقة الأحداث: حلقة تكرارية لا تتوقف.
حلقة الأحداث هي الحلقة المسؤولة عن تشغيل الوظائف المتزامنة asynchronous functions، فنحن بحاجة إلى شيء يدير كل الوظائف المتزامنة. لنرى مثالا!
سنكتب وظيفة ترحب بنا، وثم تنتظر ثانية وثم تطبع (أنا هنا):
import asyncio
async def func():
print("Hey....")
await asyncio.sleep(1)
print("I am here...")
asyncio.run(func())
في السطر الأول استدعينا المكتبة asnycio، ولعلك لاحظت الأسلوب الجديد في كتابة الوظائف المتزامنة. ففي تعريفنا لوظيفة متزامنة نستعمل الكلمة المفتاحية async، اختصارًا للتزامن asynchronous، لنخبر اللغة أن هذه وظيفة متزامنة.
ثم في السطر الخامس:
await asyncio.sleep(1)
استعملنا كلمة await، أي انتظر، وهذا ثاني مَعْلَمٍ وضعناه لنخبر اللغة بأن الوظيفة ستتأخر في هذا الجزء تحديدًا.
ثم السطر الأخير:
asyncio.run(func())
يستدعي حلقة الأحداث ونمرر لها الوظيفة لتشغلها.
لو حاولت تشغيل هذه الوظيفة دون حلقة الأحداث فإنها لن تعمل، لأن الوظائف المتزامنة تحتاج إلى حلقة أحداث، والوظائف المتزامنة لها نوع خاص في بايثون اسمه مساق مشارك coroutine.
ووجب التنبيه أن الكلمة المفتاحية await لا تستعمل سوى داخل دالة ولن تعمل خارجها، بل سيظهر لك خطأ.
ولعلك لاحظت تجاهلي للسطر:
await asyncio.sleep(1)
أتعلم ما سبب استعمالي لدالة التوقف sleep التي في مكتبة asyncio لا التي في مكتبة الوقت time؟
مكتبة asnycio له بعض الحالات التي لن تعمل بها، وهي كما في الفقرة التالية:
حالات لا يجب استعمال asyncio فيها
إذا كنت تستعمل وظيفة متزامنة فتنجب استعمال:
١. طلب باستعمال مكتبة requests، بل استعمل المكتبة المتزامنة httpx
٢. القراءة من ملف باستعمال open، بل استعمل المكتبة المتزامنة aiofile
٣. استعمال الانتظار أو الإيقاف المؤقت sleeping في مكتبة time، بل استعمل asyncio.sleep
٤. أي عملات إدخال وإخراج
إذا خلطت أي مهمات تحظر الإدخال والإخراج blocking IO في داخل وظيفة متزامنة فإن الدالة ستتحول إلى دالة غير متزامنة ستوقف برنامج حتى تنتهي، وهذا يسمى blocking IO، أي حظر الدخل والخرج حتى انتهاء المهمة.
مثال آخر
المثال الأول لا يستدعي إلا وظيفة متزامنة لا فائدة من كتابتها سوى أن تفهم أسلوب كتابة الوظائف المتزامنة.
انظر إلى هذا الرِّماز، وهو رِّماز مُحَسَّن من المثال الأول ليشغل الوظيفة ثلاث مرات:
import asyncio
async def write():
print("Hey")
await asyncio.sleep(1)
print("there")
async def main():
await asyncio.gather(write(), write(), write())
if __name__ == "__main__":
import time
start = time.perf_counter()
asyncio.run(main())
elapsed = time.perf_counter() - start
print(f"File executed in {elapsed:0.2f} seconds")
كتبنا وظيفتنا المتزامنة السابقة
ثم كتبنا وظيفة جديدة متزامنة اسمها main. داخل هذه الوظيفة استعملنا الكلمة المفتاحية await لنخبر لغة بايثون، بل بالتحديد حلقة الأحداث، أنها هنا ستتوقف، ولأنها ستتوقف فلا تطبقي حظر الدخل والخرج blocking IO فتعيقي سير البرنامج، بل واصل سير البرنامج بتطبيق التزامن.
ثم في نفس السطر استدعينا الوظيفة gather من المكتبة، ومررنا لها الوظيفة write ثلاثة مرات، كي تشغل الوظيفة ثلاثة مرات.
الوظيفة gather تمكننا من جدولة المهمات المُمَررة إليها فتطبق التزامن Concurrency.
ويجدر الإشارة إلى أن الوظيفة gather تهتم بترتيب التنفيذ، فإنها ستنفذ المهمات بالترتيب، والضد منها الوظيفة as_completed، التي لا تعبأ بترتيب التنفيذ.
شغل البرنامج لترى النتيجة:

وكما ترى، تنفذ البرنامج في ثانية واحدة!
ثلاثة وظائف تنتظر لثانية كل مرة، والتي مجموعها ثلاث ثوانٍ، تنفذت في ثانية واحدة، وهذه هي قوة التزامن!
لنكتب نفس البُرَيمِج بالأسلوب التوالي synchronous:
import time
def write():
print("Hey")
time.sleep(1)
print("there")
def main():
for _ in range(3):
write()
if __name__ == "__main__":
start = time.perf_counter()
main()
elapsed = time.perf_counter() - start
print(f"File executed in {elapsed:0.2f} seconds")
جربه لترى، سيستغرق ثلاث ثوانٍ كاملة!
الخاتمة
وها أنا أخط بقلمي الخطوط الأخيرة لهذا المقال الشائق، وأرجو إني قد وفِّقت في الشرح.
وفي نهاية الأمر لا يسعني سوى أن أشكرك على حسن قراءتك لهذا المقال، وأني لبشر أصيب وأخطِئ، فإن وفِّقت في طرح الموضوع فمن اللّٰه عز وجل وإن أخفقت فمن نفسي والشيطان.
أرجو منك تقييم كفاءة المعلومات من أجل تزويدي بالملاحظات والنقد البناء في خانة التعليقات أو عبر حساب الموقع، والسلام عليكم ورحمة اللّٰه تعالى وبركاته.