مع ظهور Python 3 ، هناك ضجة كبيرة حول "التزامن" و "التزامن" ، يمكننا أن نفترض أن Python قدم هذه الميزات / المفاهيم مؤخرًا. لكن الأمر ليس كذلك. لقد استخدمنا هذه العمليات عدة مرات. بالإضافة إلى ذلك ، قد يعتقد المبتدئون أن Asyncio هو الطريقة الوحيدة أو الأفضل لإعادة إنشاء واستخدام العمليات غير المتزامنة / المتوازية. في هذه المقالة ، سننظر في طرق مختلفة لتحقيق التوازي ومزاياها وعيوبها.
تعريف المصطلحات:
قبل الخوض في الجوانب التقنية ، من المهم أن يكون لديك بعض الفهم الأساسي للمصطلحات المستخدمة غالبًا في هذا السياق.
متزامن وغير متزامن:في العمليات
المتزامنة ، يتم تنفيذ المهام واحدة تلو الأخرى. في المهام
غير المتزامنة يمكن أن تبدأ وتكمل بشكل مستقل عن بعضها البعض. يمكن أن تبدأ مهمة غير متزامنة وتستمر في التشغيل أثناء انتقال التنفيذ إلى مهمة جديدة.
لا تحظر المهام غير المتزامنة (لا تجبر على انتظار اكتمال المهمة) وعادة ما يتم تنفيذها في الخلفية.
على سبيل المثال ، يجب عليك الاتصال بوكالة سفر للتخطيط لعطلتك القادمة. تحتاج إلى إرسال خطاب إلى مشرفك قبل السفر. في الوضع المتزامن ، تتصل أولاً بوكالة السفر ، وإذا طُلب منك الانتظار ، فسوف تنتظر حتى يجيبوك. ثم ستبدأ في كتابة رسالة إلى القائد. وهكذا ، تقوم بإكمال المهام واحدة تلو الأخرى.
[تنفيذ متزامن ، تقريبا. مترجم] ولكن ، إذا كنت ذكيًا ، فقد طلبوا منك الانتظار
[تعليق على الهاتف ، تقريبًا. المترجم] ستبدأ في كتابة البريد الإلكتروني وعندما تتحدث مرة أخرى ستتوقف الكتابة والكتابة ثم تضيف الرسالة. يمكنك أيضًا أن تطلب من صديق الاتصال بالوكالة وكتابة خطاب بنفسك. هذا هو التزامن ، والمهام لا تمنع بعضها البعض.
القدرة التنافسية والتوافق:تعني القدرة التنافسية أن يتم تنفيذ مهمتين بشكل
مشترك . في مثالنا السابق ، عندما نظرنا في المثال غير المتزامن ، تقدمنا تدريجيًا في كتابة رسالة ، ثم في محادثة مع جولة. وكالة. هذه
تنافسية .
عندما طلبنا الاتصال بصديق وكتبنا رسالة بأنفسنا ، تم تنفيذ المهام
بالتوازي .
التزامن هو في الأساس شكل من أشكال المنافسة. لكن التزامن يعتمد على الأجهزة. على سبيل المثال ، إذا كانت وحدة المعالجة المركزية تحتوي على نواة واحدة فقط ، فلا يمكن تنفيذ مهمتين بالتوازي. إنهم ببساطة يشاركون وقت المعالج فيما بينهم. إذن هذه منافسة ، لكن ليس التزامن. ولكن عندما يكون لدينا العديد من النوى
[كصديق في المثال السابق ، وهو النواة الثانية ، تقريبًا. مترجم] يمكننا إجراء عدة عمليات (حسب عدد النوى) في نفس الوقت.
لتلخيص:
- التزامن: حظر العمليات (الحجب)
- عدم التزامن: لا يمنع العمليات (عدم الحظر)
- القدرة التنافسية: التقدم المشترك (مشترك)
- التزامن: التقدم الموازي (الموازي)
التزامن يعني المنافسة. لكن المنافسة لا تعني دائمًا التوافق.
الخيوط والعمليات
تدعم Python سلاسل الرسائل لفترة طويلة جدًا. تتيح لك السلاسل تنفيذ العمليات بشكل تنافسي. ولكن هناك مشكلة في
Global Interpreter Lock (GIL) نظرًا لأن سلاسل المحادثات لا يمكنها توفير التزامن حقيقي. ومع ذلك ، مع ظهور
المعالجة المتعددة ، يمكنك استخدام نوى متعددة باستخدام Python.
خيوطفكر في مثال صغير. في الكود التالي ، سيتم تنفيذ وظيفة
العامل على سلاسل محادثات متعددة بشكل غير متزامن ومتزامن.
import threading import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!")
وإليك مثال على الإخراج:
$ python thread_test.py All Threads are queued, let's see when they finish! I am Worker 1, I slept for 1 seconds I am Worker 3, I slept for 4 seconds I am Worker 4, I slept for 5 seconds I am Worker 2, I slept for 7 seconds I am Worker 0, I slept for 9 seconds
وهكذا ، بدأنا 5 سلاسل رسائل للتعاون وبعد أن تبدأ (أي بعد إطلاق وظيفة العامل) ،
لا تنتظر العملية اكتمال سلاسل العمليات قبل الانتقال إلى عبارة الطباعة التالية. هذه عملية غير متزامنة.
في مثالنا ، قمنا بتمرير الوظيفة إلى مُنشئ Thread. إذا أردنا ، يمكننا تنفيذ فئة فرعية بطريقة (أسلوب OOP).
مزيد من القراءة:لمعرفة المزيد حول التدفقات ، استخدم الرابط أدناه:
قفل المترجم العالمي (GIL)تم تقديم GIL لتسهيل معالجة ذاكرة CPython وتوفير أفضل تكامل مع C (على سبيل المثال مع الملحقات). GIL هي آلية قفل عندما يقوم مترجم Python بتشغيل مؤشر ترابط واحد فقط في كل مرة. على سبيل المثال يمكن تنفيذ مؤشر ترابط واحد فقط في Pyton bytecode في كل مرة. يضمن GIL عدم تنفيذ سلاسل عمليات متعددة
بالتوازي .
تفاصيل سريعة GIL:
- يمكن تشغيل مؤشر ترابط واحد في كل مرة.
- يقوم مترجم Python بالتبديل بين الخيوط لتحقيق القدرة التنافسية.
- GIL قابل للتطبيق على CPython (التنفيذ القياسي). ولكن مثل ، على سبيل المثال ، Jython و IronPython ليس لديهم GIL.
- GIL يجعل البرامج ذات الخيوط الفردية سريعة.
- عادة لا تتداخل GIL مع I / O.
- GIL يجعل من السهل دمج المكتبات الآمنة لمؤشر الترابط في C ، بفضل GIL لدينا العديد من الإضافات / الوحدات عالية الأداء المكتوبة في C.
- بالنسبة للمهام التابعة لوحدة المعالجة المركزية ، يقوم المترجم بفحص كل علامات N وتبديل سلاسل العمليات. لذلك ، لا يمنع أحد مؤشرات الترابط الأخرى.
يرى الكثير من GIL ضعف. أعتبر هذا نعمة ، لأن مكتبات مثل NumPy ، SciPy تم إنشاؤها ، والتي تحتل مكانة خاصة وفريدة في المجتمع العلمي.
مزيد من القراءة:ستسمح لك هذه الموارد بالتعمق في GIL:
العملياتلتحقيق التزامن في Python ، تمت إضافة وحدة
معالجة متعددة توفر واجهة برمجة تطبيقات وتبدو متشابهة جدًا إذا استخدمت
الترابط من قبل.
دعنا نذهب فقط وتغيير المثال السابق. يستخدم الإصدار المعدل الآن
العملية بدلاً من
الدفق .
import multiprocessing import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!")
ما الذي تغير؟ لقد قمت للتو باستيراد وحدة
المعالجة المتعددة بدلاً من
الترابط . ثم ، بدلاً من خيط ، استخدمت عملية. هذا كل شيء! الآن ، بدلاً من العديد من سلاسل الرسائل ، نستخدم العمليات التي تعمل على نوى مختلفة لوحدة المعالجة المركزية (ما لم يكن للمعالج بالطبع عدة نوى).
باستخدام فئة Pool ، يمكننا أيضًا توزيع تنفيذ وظيفة واحدة بين عدة عمليات لقيم إدخال مختلفة. مثال من المستندات الرسمية:
from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3]))
هنا ، بدلاً من التكرار على قائمة القيم واستدعاء الوظيفة f واحدًا تلو الآخر ، نقوم فعليًا بتشغيل الوظيفة في عمليات مختلفة. تقوم إحدى العمليات بـ f (1) ، والأخرى f (2) ، والأخرى f (3). أخيرًا ، يتم دمج النتائج مرة أخرى في قائمة. هذا يسمح لنا بتقسيم الحسابات الثقيلة إلى أجزاء أصغر وتشغيلها بالتوازي لحساب أسرع.
مزيد من القراءة:وحدة المتزامنةالوحدة النمطية concurrent.futures كبيرة وتجعل كتابة التعليمات البرمجية غير المتزامنة أمرًا سهلاً للغاية. المفضلة هي
ThreadPoolExecutor و
ProcessPoolExecutor . هؤلاء الفنانين يدعمون مجموعة من الخيوط أو العمليات. نرسل مهامنا إلى التجمع ، ويقوم بتشغيل المهام في سلسلة / عملية يمكن الوصول إليها. يتم إرجاع كائن
المستقبل الذي يمكن استخدامه للاستعلام عن النتيجة واستردادها عند اكتمال المهمة.
وإليك مثال ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello")) print(future.done()) sleep(5) print(future.done()) print(future.result())
عندي مقال حول concurrent.futures
masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html . يمكن أن يكون مفيدا لدراسة أعمق لهذه الوحدة.
مزيد من القراءة:Asyncio - ماذا وكيف ولماذا؟
ربما لديك سؤال لدى العديد من الأشخاص في مجتمع Python - ما الذي يجلبه Asyncio جديدًا؟ لماذا كانت هناك طريقة أخرى لاستخدام الإدخال / الإخراج غير المتزامن؟ أليس لدينا بالفعل خيوط وعمليات؟ دعنا نرى!
لماذا نحتاج إلى Asyncio؟العمليات مكلفة للغاية
[من حيث استهلاك الموارد ، تقريبا. مترجم] لخلق. لذلك ، لعمليات الإدخال / الإخراج ، يتم تحديد مؤشرات الترابط بشكل أساسي. نحن نعلم أن الإدخال / الإخراج يعتمد على الأشياء الخارجية - محركات الأقراص البطيئة أو تباطؤ الشبكة غير السار يجعل I / O غير متوقعة في كثير من الأحيان. افترض الآن أننا نستخدم خيوط لـ I / O. تؤدي 3 سلاسل عمليات مختلفة لمهام الإدخال / الإخراج. سيتعين على المترجم التبديل بين التدفقات التنافسية وإعطاء كل منهم بعض الوقت بدوره. استدعاء التدفقات T1 و T2 و T3. بدأت ثلاثة مؤشرات ترابط عمليات الإدخال / الإخراج. يكمله T3 أولاً. T2 و T1 لا يزالون بانتظار I / O. يتحول مترجم Python إلى T1 ، لكنه لا يزال ينتظر. حسنًا ، ينتقل المترجم إلى T2 ، ولا يزال المترجم ينتظر ، ثم ينتقل إلى T3 ، وهو جاهز وينفذ الشفرة. هل ترى هذا كمشكلة؟
كان T3 جاهزًا ، لكن المترجم انتقل أولاً بين T2 و T1 - وهذا تكبد تكاليف التحويل ، والتي كان بإمكاننا تجنبها إذا تحول المترجم أولاً إلى T3 ، أليس كذلك؟
ما هو اسينيو؟يوفر لنا Asyncio حلقة حدث جنبًا إلى جنب مع أشياء رائعة أخرى. تراقب حلقة الأحداث أحداث I / O وتبديل المهام الجاهزة والانتظار لعمليات الإدخال / الإخراج
[حلقة الحدث هي بنية برمجية تنتظر الوصول وترسل الأحداث أو الرسائل في البرنامج تقريبًا. مترجم] .
الفكرة بسيطة للغاية. هناك حلقة حدث. ولدينا وظائف تؤدي أداء I / O غير متزامن. ننقل وظائفنا إلى حلقة الحدث ونطلب منه تشغيلها لنا. تعيدنا حلقة الحدث إلى كائن المستقبل ، مثل الوعد بأننا سنحصل في المستقبل على شيء ما. نتمسك بوعدنا ، نتحقق من وقت لآخر ما إذا كان ذلك مهمًا (لا يمكننا الانتظار حقًا) ، وأخيرًا ، عندما يتم استلام القيمة ، نستخدمها في بعض العمليات الأخرى
[أي أرسلنا طلبًا ، وتم إعطاؤنا تذكرة فورًا وطلب منا الانتظار حتى تأتي النتيجة. نتحقق من النتيجة بشكل دوري ، وبمجرد استلامها ، نأخذ تذكرة ونحصل عليها بقيمة ، تقريبًا. مترجم] .
يستخدم Asyncio المولدات والكوريونات لإيقاف واستئناف المهام. يمكنك قراءة التفاصيل هنا:
كيفية استخدام Asyncio؟قبل أن نبدأ ، دعنا نلقي نظرة على مثال:
import asyncio import datetime import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever()
لاحظ أن بناء جملة async / await خاص بـ Python 3.5 والإصدارات الأحدث فقط. دعنا نذهب من خلال الرمز:
- لدينا وظيفة display_date غير متزامنة تأخذ رقمًا (كمعرف) وحلقة حدث كمعلمات.
- تحتوي الوظيفة على حلقة لا نهائية ، يتم مقاطعتها بعد 50 ثانية. ولكن خلال هذه الفترة ، تطبع الوقت بشكل متكرر وتتوقف مؤقتًا. يمكن أن تنتظر وظيفة الانتظار انتظار اكتمال الوظائف الأخرى غير المتزامنة (coroutine).
- نقوم بتمرير الوظيفة إلى حلقة الأحداث (باستخدام طريقة ضمان_ال مستقبل).
- نبدأ دورة من الأحداث.
عندما يتم استدعاء الانتظار ، يدرك Asyncio أن الوظيفة ستستغرق بعض الوقت على الأرجح. وبالتالي ، فإنه يوقف التنفيذ مؤقتًا ويبدأ في مراقبة أي أحداث I / O مرتبطة به ويسمح لك بتشغيل المهام. عندما يلاحظ asyncio أن الدالة I / O المتوقفة مؤقتًا جاهزة ، فإنها تستأنف الوظيفة.
القيام بالاختيار الصحيح.
لقد مررنا للتو بأشهر أشكال التنافسية. ولكن يبقى السؤال - ما الذي يجب اختياره؟ يعتمد ذلك على حالات الاستخدام. من تجربتي ، أميل إلى اتباع هذا الرمز الزائف:
if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads") else: print("Multi Processing")
- حدود وحدة المعالجة المركزية => معالجة متعددة
- ربط I / O ، إدخال / إخراج سريع ، عدد محدود من الاتصالات => تعدد سلاسل المحادثات
- مدخلات I / O ، بطيئة I / O ، العديد من الاتصالات => Asyncio
[ملاحظة مترجم]