ترجمة الفصل 13 التزامن
من كتاب "خبير بيثون البرمجة" ،
الطبعة الثانية
ميخائيل جاورسكي وطارق زياد ، 2016
البرمجة غير المتزامنة
في السنوات الأخيرة ، اكتسبت البرمجة غير المتزامنة شعبية كبيرة. حصلت Python 3.5 أخيرًا على بعض وظائف بناء الجملة التي تعزز مفاهيم الحلول غير المتزامنة. ولكن هذا لا يعني أن البرمجة غير المتزامنة أصبحت ممكنة فقط منذ بيثون 3.5. تم توفير العديد من المكتبات والأطر في وقت مبكر ، ومعظمها نشأ في الإصدارات القديمة من بيثون 2. حتى أن هناك تطبيقًا بديلاً كاملًا لبيثون يسمى Stackless (انظر الفصل 1 ، "الوضع الحالي لبيثون") ، والذي يركز على نهج البرمجة الفردي هذا. بالنسبة لبعض الحلول ، مثل
Twisted أو Tornado أو
Eventlet ، لا تزال المجتمعات النشطة موجودة وتستحق المعرفة حقًا. في أي حال ، بدءًا من Python 3.5 ، أصبحت البرمجة غير المتزامنة أسهل من أي وقت مضى. وبالتالي ، من المتوقع أن تحل وظائفها غير المتزامنة المدمجة محل معظم الأدوات القديمة ، أو ستتحول المشاريع الخارجية تدريجياً إلى نوع من الأطر رفيعة المستوى تعتمد على بيثون المدمجة.
عند محاولة شرح ماهية البرمجة غير المتزامنة ، من الأسهل التفكير في هذا النهج كشيء مشابه لمؤشرات الترابط ، ولكن دون جدولة نظام. هذا يعني أن البرنامج غير المتزامن يمكنه معالجة المهام في نفس الوقت ، ولكن يتم تبديل سياقه داخليًا وليس بواسطة برنامج جدولة النظام.
ولكن ، بالطبع ، لا نستخدم مؤشرات الترابط للمعالجة المتوازية للمهام في برنامج غير متزامن. تستخدم معظم الحلول مفاهيم مختلفة ، ووفقًا للتنفيذ ، تسمى بطريقة مختلفة. فيما يلي بعض أمثلة الأسماء المستخدمة لوصف كائنات البرنامج المتوازية:
- خيوط خضراء - خيوط خضراء (مشاريع greenlet ، gevent أو eventlet)
- Coroutines - coroutines (البرمجة غير المتزامنة البحتة في Python 3.5)
- Tasklets (Stackless Python) هذه هي في الأساس نفس المفاهيم ، ولكنها غالباً ما تنفذ بطرق مختلفة قليلاً.
لأسباب واضحة ، سنركز في هذا القسم فقط على coroutines المدعومة مبدئيًا بواسطة Python ، بدءًا من الإصدار 3.5.
تعدد المهام التعاونية و I / O غير متزامن
تعدد المهام التعاونية هو جوهر البرمجة غير المتزامنة. وبهذا المعنى ، فإن تعدد المهام في نظام التشغيل ليس مطلوبًا لبدء تبديل للسياق (إلى عملية أو مؤشر ترابط آخر) ، ولكن بدلاً من ذلك ، تطلق كل عملية طواعية التحكم عندما تكون في وضع الاستعداد لضمان التنفيذ المتزامن لعدة برامج. هذا هو السبب يطلق عليه التعاونية. يجب أن تعمل جميع العمليات معًا لضمان نجاح تعدد المهام.
تم استخدام نموذج تعدد المهام في بعض الأحيان في أنظمة التشغيل ، ولكن الآن لا يمكن العثور عليه كحل على مستوى النظام. هذا لأنه يوجد خطر من أن خدمة سيئة التصميم يمكن أن تزعزع بسهولة استقرار النظام بأكمله. تعد جدولة مؤشرات الترابط والعمليات باستخدام مفاتيح تبديل السياق التي يتحكم فيها مباشرة نظام التشغيل هي الطريقة السائدة في التزامن على مستوى النظام. لكن تعدد المهام التعاونية لا يزال أداة التزامن كبيرة على مستوى التطبيق.
الحديث عن تعدد المهام المشتركة على مستوى التطبيق ، نحن لا نتعامل مع مؤشرات الترابط أو العمليات التي تحتاج إلى تحرير التحكم ، لأن كل التنفيذ موجود في عملية واحدة ومؤشر ترابط. بدلاً من ذلك ، لدينا العديد من المهام (coroutines ومهام المهام والخيوط الخضراء) التي تنقل التحكم إلى وظيفة واحدة تتحكم في تنسيق المهام. هذه الوظيفة هي عادة نوع من حلقة الحدث.
لتجنب الالتباس (بسبب مصطلحات بيثون) ، سوف ندعو الآن مثل هذه المهام المتوازية coroutines. المشكلة الأكثر أهمية في تعدد المهام التعاوني هي متى يتم نقل التحكم. في معظم التطبيقات غير المتزامنة ، يتم تمرير التحكم إلى المجدول أو حلقة الحدث أثناء عمليات الإدخال / الإخراج. بغض النظر عما إذا كان البرنامج يقرأ البيانات من نظام الملفات أو يتصل عبر مأخذ توصيل ، فإن عملية الإدخال / الإخراج هذه ترتبط دائمًا بوقت انتظار عندما تصبح العملية غير نشطة. يعتمد الكمون على مورد خارجي ، لذا فهذه فرصة جيدة لتحرير التحكم حتى يتمكن كوريون آخرون من أداء عملهم ، حتى يتعين عليهم انتظار أن يكون هذا النهج مشابهًا إلى حد ما في السلوك لكيفية تنفيذ تعدد العمليات في بيثون. نعلم أن GIL تقوم بتسلسل خيوط Python ، لكن يتم تحريرها أيضًا مع كل عملية إدخال / إخراج. يتمثل الاختلاف الرئيسي في أنه يتم تنفيذ مؤشرات الترابط في Python باعتبارها مؤشرات ترابط على مستوى النظام ، بحيث يمكن لنظام التشغيل تفريغ مؤشر الترابط الجاري حاليًا في أي وقت ونقل التحكم إلى آخر.
في البرمجة غير المتزامنة ، لا تنقطع المهام عن طريق حلقة الحدث الرئيسية. لهذا السبب يسمى أسلوب تعدد المهام هذا أيضًا تعدد المهام غير ذي الأولوية.
بالطبع ، كل تطبيق Python يعمل على نظام تشغيل حيث توجد عمليات أخرى تتنافس على الموارد. هذا يعني أن نظام التشغيل لديه دائمًا الحق في إلغاء تحميل العملية بالكامل ونقل التحكم إلى آخر. ولكن عندما يبدأ تطبيقنا غير المتزامن مرة أخرى ، فإنه يستمر من حيث تم إيقافه مؤقتًا عندما تدخل برنامج جدولة النظام. هذا هو السبب في أن الكوروتينات في هذا السياق تعتبر غير مزدحمة.
بيثون متزامن وينتظر الكلمات الرئيسية
الكلمات الأساسية غير
المتزامنة والانتظار هي اللبنات الأساسية في برمجة Python غير المتزامنة.
الكلمة الأساسية غير المتزامن المستخدمة قبل بيان
def تحدد coroutine جديد. يمكن تعليق وظيفة coroutine واستئنافها في ظل ظروف محددة بدقة. يشبه بناء الجملة والسلوكيات المولدات (راجع الفصل 2 ، "توصيات بناء الجملة" ، أسفل مستوى الفصل). في الواقع ، يجب استخدام المولدات في الإصدارات القديمة من Python لتنفيذ coroutines. فيما يلي مثال لإعلان دالة تستخدم
الكلمة الأساسية غير المتزامنة :
async def async_hello(): print("hello, world!")
الوظائف المحددة باستخدام
الكلمة الأساسية غير
المتزامنة خاصة. عند الاتصال ، لا يقومون بتنفيذ التعليمات البرمجية في الداخل ، ولكن بدلاً من ذلك يعرضون كائن coroutine:
>>>> async def async_hello(): ... print("hello, world!") ... >>> async_hello() <coroutine object async_hello at 0x1014129e8>
لا يقوم كائن coroutine بأي شيء حتى يتم جدولة تنفيذه في حلقة الحدث. تتوفر الوحدة غير المتزامنة لتوفير تنفيذ أساسي لحلقة الحدث ، بالإضافة إلى العديد من الأدوات المساعدة غير المتزامنة الأخرى:
>>> import asyncio >>> async def async_hello(): ... print("hello, world!") ... >>> loop = asyncio.get_event_loop() >>> loop.run_until_complete(async_hello()) hello, world! >>> loop.close()
بطبيعة الحال ، إنشاء coroutine واحد فقط بسيط ، في برنامجنا نحن لا نطبق التوازي. لرؤية شيء متوازٍ حقًا ، نحتاج إلى إنشاء المزيد من المهام التي سيتم تنفيذها بواسطة حلقة حدث.
يمكن إضافة مهام جديدة إلى الحلقة عن طريق استدعاء الأسلوب
loop.create_task () أو عن طريق توفير كائن آخر لانتظار
استخدام وظيفة
asyncio.wait () . سنستخدم الطريقة الأخيرة ونحاول طباعة تسلسل من الأرقام التي تم إنشاؤها باستخدام دالة
range () بشكل غير متزامن:
import asyncio async def print_number(number): print(number) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete( asyncio.wait([ print_number(number) for number in range(10) ]) ) loop.close()
تقبل الدالة
asyncio.wait () قائمة من الكائنات coroutine وتعود على الفور. والنتيجة هي مولد ينتج كائنات تمثل النتائج المستقبلية (العقود المستقبلية). كما يوحي الاسم ، يتم استخدامه لانتظار استكمال جميع coroutines المقدمة. سبب إرجاع المولد بدلاً من كائن coroutine هو أنه متوافق مع الإصدارات السابقة من Python ، والذي سيتم شرحه لاحقًا. قد تكون نتيجة تنفيذ هذا البرنامج النصي كما يلي:
$ python asyncprint.py 0 7 8 3 9 4 1 5 2 6
كما نرى ، لا تتم طباعة الأرقام بالترتيب الذي أنشأنا به coroutines لدينا. ولكن هذا هو بالضبط ما أردنا تحقيقه.
الكلمة الرئيسية الهامة الثانية المضافة في Python 3.5 هي في
انتظار . يتم استخدامه لانتظار نتائج حدث coroutine أو حدث في المستقبل (تم شرحه لاحقًا) وتحرير التحكم في التنفيذ في حلقة الحدث. لفهم كيفية عمل هذا بشكل أفضل ، نحتاج إلى التفكير في مثال رمز أكثر تعقيدًا.
لنفترض أننا نريد إنشاء اثنين من coroutines التي ستؤدي بعض المهام البسيطة في حلقة:
- انتظر عدد عشوائي من الثواني
- قم بطباعة بعض النصوص المقدمة كوسيطة ، ومقدار الوقت المستغرق في الانتظار. لنبدأ بتنفيذ بسيط يحتوي على بعض مشكلات التزامن التي سنحاول تحسينها لاحقًا باستخدام إضافي للانتظار:
import time import random import asyncio async def waiter(name): for _ in range(4): time_to_sleep = random.randint(1, 3) / 4 time.sleep(time_to_sleep) print( "{} waited {} seconds" "".format(name, time_to_sleep) ) async def main(): await asyncio.wait([waiter("foo"), waiter("bar")]) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
عند تنفيذه في الجهاز (باستخدام الأمر time لقياس الوقت) ، يمكنك رؤية:
$ time python corowait.py bar waited 0.25 seconds bar waited 0.25 seconds bar waited 0.5 seconds bar waited 0.5 seconds foo waited 0.75 seconds foo waited 0.75 seconds foo waited 0.25 seconds foo waited 0.25 seconds real 0m3.734s user 0m0.153s sys 0m0.028s
كما نرى ، فقد أتم كلاهما تنفيذهما ، لكن ليس بشكل غير متزامن. السبب هو أنهم يستخدمون الدالة
time.sleep () ، والتي تقوم بتأمين ولكن لا يتم تحرير عنصر التحكم في حلقة الحدث. سيعمل هذا بشكل أفضل في تثبيت متعدد الخيوط ، لكننا لا نريد استخدام التدفقات في الوقت الحالي. إذا كيف يمكننا إصلاح هذا؟
الإجابة هي استخدام
asyncio.sleep () ، وهو إصدار غير متزامن من time.sleep () ، ونتوقع النتيجة باستخدام الكلمة الرئيسية
await . لقد استخدمنا هذا البيان بالفعل في الإصدار الأول من
main () ، ولكن هذا كان فقط لتحسين وضوح الكود. من الواضح أن هذا لم يجعل تنفيذنا أكثر توازنا. دعونا نلقي نظرة على نسخة محسنة من coroutine
النادل () الذي يستخدم في انتظار asyncio.sleep ():
async def waiter(name): for _ in range(4): time_to_sleep = random.randint(1, 3) / 4 await asyncio.sleep(time_to_sleep) print( "{} waited {} seconds" "".format(name, time_to_sleep) )
عند تشغيل البرنامج النصي المحدّث ، سنرى كيف يتناوب إخراج وظيفتين مع بعضهما البعض:
$ time python corowait_improved.py bar waited 0.25 seconds foo waited 0.25 seconds bar waited 0.25 seconds foo waited 0.5 seconds foo waited 0.25 seconds bar waited 0.75 seconds foo waited 0.25 seconds bar waited 0.5 seconds real 0m1.953s user 0m0.149s sys 0m0.026s
فائدة إضافية لهذا التحسين البسيط هي أن الشفرة تعمل بشكل أسرع. كان إجمالي وقت التنفيذ أقل من مجموع جميع أوقات النوم ، لأن سيطروا على واحد من واحد تلو الآخر.
Asyncio في الإصدارات السابقة من بيثون
ظهرت الوحدة غير المتماثلة في بيثون 3.4. هذه هي النسخة الوحيدة من بيثون التي لديها دعم جاد للبرمجة غير المتزامنة قبل بيثون 3.5. لسوء الحظ ، يبدو أن هذين الإصدارين اللاحقين يكفيان لعرض مشكلات التوافق.
على أي حال ، تم تقديم أساس البرمجة غير المتزامن في Python قبل عناصر بناء الجملة التي تدعم هذا القالب. متأخرا أفضل من ألا تأتي أبدا ، ولكن هذا خلق وضعا حيث يوجد اثنين من بناء الجملة للعمل مع coroutines.
بدءًا من Python 3.5 ، يمكنك استخدام
المزامنة والانتظار :
async def main (): await asyncio.sleep(0)
ومع ذلك ، في بيثون 3.4 ، سوف تضطر إلى تطبيق الديكور asyncio.coroutine بالإضافة إلى ذلك في النص coroutine:
@asyncio.couroutine def main(): yield from asyncio.sleep(0)
حقيقة أخرى مفيدة هي أن
العائد من البيان تم تقديمه في Python 3.3 ، وأن PyPI لديها خلفية غير متزامنة. هذا يعني أنه يمكنك أيضًا استخدام هذا التطبيق لتعدد المهام التعاوني مع Python 3.3.
مثال عملي للبرمجة غير المتزامنة
كما ذكرنا عدة مرات في هذا الفصل ، تعد البرمجة غير المتزامنة أداة رائعة للتعامل مع الإدخال / الإخراج. لقد حان الوقت لإنشاء شيء أكثر عملية من مجرد طباعة تسلسلات أو انتظار غير متزامن.
لضمان الاتساق ، سنحاول حل نفس المشكلة التي قمنا بحلها بمساعدة تعدد العمليات والمعالجة المتعددة. لذلك ، سنحاول استخراج بعض البيانات بشكل غير متزامن من الموارد الخارجية من خلال اتصال الشبكة. سيكون من الرائع أن نتمكن من استخدام نفس حزمة
بيثون-غمابس كما في الأقسام السابقة. لسوء الحظ ، لا يمكننا ذلك.
كان خالق
الثعبان gmaps كسولًا قليلًا ولم يأخذ سوى الاسم. لتبسيط التطوير ، اختار حزمة الطلب كمكتبة عميل HTTP الخاصة به. لسوء الحظ ، لا تدعم الطلبات الإدخال / الإخراج غير
المتزامن مع
المزامنة وتنتظر . هناك بعض المشاريع الأخرى التي تهدف إلى توفير بعض التوازي لمشروع الاستعلام ، ولكنها تعتمد إما على
Gevent (
grequests ، راجع
https://github.com/ kennethreitz / grequests ) أو تشغيل تجمع
مؤشرات ترابط / عملية (استعلام - العقود المستقبلية الرجوع إلى
github.com/ross/requests-futures ). لا أحد منهم يحل مشكلتنا.
قبل توبيخ نفسي لتوبيخ مطور بريء مفتوح المصدر ، تهدأ. الشخص الذي يقف وراء حزمة python-gmaps هو أنا. اختيار الفقراء من التبعيات هي واحدة من مشاكل هذا المشروع. أود فقط أن أنتقد نفسي علانية من وقت لآخر. سيكون هذا درسًا مريرًا بالنسبة لي ، حيث لا يمكن دمج python-gmaps في أحدث إصدار (0.3.1 وقت كتابة هذا الكتاب) بسهولة مع I / O غير المتزامن في Python. في أي حال ، قد يتغير هذا في المستقبل ، لذلك لا يضيع شيء.
مع العلم بالقيود المفروضة على المكتبة ، والتي كانت سهلة الاستخدام في الأمثلة السابقة ، نحتاج إلى إنشاء شيء يسد هذه الفجوة. Google MapsAPI سهل الاستخدام حقًا ، لذلك سنقوم بضبط أداة غير متزامنة للتوضيح فقط. لا تزال المكتبة القياسية لبيثون 3.5 تفتقر إلى مكتبة يمكنها تنفيذ طلبات HTTP غير المتزامنة بسهولة مثل استدعاء
urllib.urlopen () . من المؤكد أننا لا نريد إنشاء دعم بروتوكول كامل من البداية ، لذلك سنستخدم القليل من المساعدة من حزمة
aiohttp المتوفرة في PyPI. هذه مكتبة واعدة حقًا تضيف كلاً من تطبيقات العميل والخادم لـ HTTP غير متزامن. فيما يلي وحدة صغيرة مبنية على أعلى
aiohttp تقوم بإنشاء وظيفة مساعد
geocode () واحدة تنفذ طلبات الترميز الجغرافي لخدمة Google Maps API:
import aiohttp session = aiohttp.ClientSession() async def geocode(place): params = { 'sensor': 'false', 'address': place } async with session.get( 'https://maps.googleapis.com/maps/api/geocode/json', params=params ) as response: result = await response.json() return result['results']
لنفترض أن هذا الرمز مخزّن في وحدة نمطية باسم
asyncgmaps ، والتي
سنستخدمها لاحقًا. نحن الآن على استعداد لإعادة كتابة المثال المستخدم في مناقشة تعدد العمليات والمعالجة المتعددة. في السابق ، اعتدنا على فصل العملية بأكملها إلى مرحلتين منفصلتين:
- قم بملء جميع الطلبات إلى الخدمة الخارجية بالتوازي باستخدام دالة fetch_place () .
- عرض جميع النتائج في حلقة باستخدام دالة present_result () .
ولكن نظرًا لأن تعدد المهام التعاونية مختلف تمامًا عن استخدام عمليات أو سلاسل عمليات متعددة ، فيمكننا تغيير نهجنا قليلاً. معظم المشكلات التي أثيرت في استخدام موضوع واحد لكل عنصر لم تعد تهمنا.
Coroutines ليست وقائية ، لذلك يمكننا بسهولة عرض النتائج مباشرة بعد تلقي ردود HTTP. هذا سوف يبسط الكود الخاص بنا ويجعله أكثر قابلية للفهم:
import asyncio
تعد البرمجة غير المتزامنة كبيرة لمطوري الواجهة الخلفية المهتمين بإنشاء تطبيقات قابلة للتطوير. في الممارسة العملية ، هذه واحدة من أهم الأدوات لإنشاء خوادم ذات قدرة تنافسية عالية.
لكن الواقع محزن. العديد من الحزم الشائعة التي تتعامل مع مشكلات I / O غير مخصصة للاستخدام مع تعليمات برمجية غير متزامنة. الأسباب الرئيسية لهذا هي:
- لا يزال التنفيذ منخفضًا لـ Python 3 وبعض ميزاته المتقدمة
- انخفاض فهم مختلف مفاهيم التوافق بين المبتدئين لتعلم بيثون
هذا يعني أنه غالبًا ما يكون ترحيل التطبيقات والحزم المتزامنة متعددة الخيوط إما مستحيلًا (بسبب القيود المعمارية) أو باهظ التكلفة. يمكن أن تستفيد العديد من المشروعات بشكل كبير من تنفيذ نمط تعدد المهام غير المتزامن ، ولكن القليل منها فقط سوف يفعل ذلك في النهاية. هذا يعني أنه سيكون لديك الآن الكثير من الصعوبات في محاولة إنشاء تطبيقات غير متزامنة من البداية. في معظم الحالات ، سيكون هذا مشابهًا للمشكلة المذكورة في قسم "مثال عملي للبرمجة غير المتزامنة" - الواجهات غير المتوافقة والحظر غير المتزامن لعمليات الإدخال / الإخراج. بالطبع ، في بعض الأحيان يمكنك التخلي عن الانتظار عندما تواجه عدم التوافق هذا والحصول على الموارد اللازمة بشكل متزامن. ولكن هذا سوف يمنع كل coroutine الآخر من تنفيذ التعليمات البرمجية الخاصة به أثناء انتظار النتائج. من الناحية الفنية ، يعمل هذا ، ولكن أيضًا يدمر كل فوائد البرمجة غير المتزامنة. وبالتالي ، في النهاية ، فإن دمج I / O غير المتزامن مع I / O المتزامن ليس خيارًا. هذه لعبة كلها أو لا شيء.
مشكلة أخرى هي عمليات طويلة منضمة المعالج. عند إجراء عملية إدخال / إخراج ، لا توجد مشكلة في تحرير التحكم من coroutine. عند الكتابة / القراءة من نظام الملفات أو المقبس ، سوف تنتظر في نهاية المطاف ، لذلك فإن المكالمة التي تستخدم في انتظار هي أفضل ما يمكنك القيام به. ولكن ماذا لو كنت بحاجة لحساب شيء ما ، وأنت تعلم أن الأمر سيستغرق بعض الوقت؟ بالطبع ، يمكنك تقسيم المشكلة إلى أجزاء وإلغاء عنصر التحكم في كل مرة تقدم فيها العمل قليلاً. ولكن سرعان ما ستجد أن هذا ليس نموذجًا جيدًا للغاية. مثل هذا الشيء يمكن أن يجعل الكود فوضويًا ، كما لا يضمن نتائج جيدة.
الربط الزمني يجب أن يكون مسؤولية المترجم أو نظام التشغيل.
دمج التعليمات البرمجية غير المتزامنة مع العقود المستقبلية غير المتزامنة
لذا ، ماذا تفعل إذا كان لديك رمز يؤدي إلى إدخال / إخراج متزامن طويل لا يمكنك أو لا ترغب في إعادة كتابته. أو ماذا تفعل عندما تضطر إلى إجراء بعض عمليات المعالجات الثقيلة في تطبيق مصمم بشكل أساسي للإدخال / الإخراج غير المتزامن؟ حسنًا ... أنت بحاجة إلى إيجاد حل بديل. وأعني بذلك تعدد العمليات أو المعالجة المتعددة.
قد لا يبدو هذا جيدًا جدًا ، ولكن في بعض الأحيان قد يكون أفضل حل هو ما حاولنا الابتعاد عنه. تتم المعالجة المتوازية للمهام كثيفة الاستخدام للموارد في بيثون دائمًا بشكل أفضل بسبب المعالجة المتعددة. ويمكن للتعددية التعامل مع عمليات الإدخال / الإخراج بشكل جيد على قدم المساواة (بسرعة ودون الكثير من الموارد) ، وغير متزامن والانتظار إذا تم تكوينها بشكل صحيح والتعامل معها بعناية.
لذا ، في بعض الأحيان ، عندما لا تعرف ما يجب القيام به عندما لا يتناسب شيء ما مع تطبيقك غير المتزامن ، استخدم جزءًا من الكود يضعه في خيط أو عملية منفصلة. يمكنك أن تتظاهر بأنها عنصر تحكم ، تحكم في تحرير حلقة الحدث ، وفي النهاية ستقوم بمعالجة النتائج عندما تكون جاهزة.
لحسن الحظ بالنسبة لنا ، توفر مكتبة Python القياسية وحدة
concurrent.futures ، والتي يتم دمجها أيضًا مع الوحدة النمطية
asyncio . تسمح لك هاتان الوحدتان معًا بالتخطيط لحظر الوظائف التي يتم تنفيذها في سلاسل العمليات أو العمليات الإضافية ، كما لو كانت coroutines غير متزامنة غير محظورة.
منفذي العقود الآجلة
قبل أن نرى كيفية تضمين سلاسل العمليات أو العمليات في حلقة حدث غير متزامن ، نلقي نظرة فاحصة على الوحدة النمطية
concurrent.futures ، والتي ستصبح فيما بعد المكون الرئيسي لحلنا المزعوم.
أهم الفئات في وحدة
concurrent.futures هي
Executor و
Future .
Executor هو مجموعة من الموارد التي يمكنها معالجة عناصر العمل بشكل متوازٍ. قد يبدو متشابهًا تمامًا في الغرض من الفئات من وحدة المعالجات المتعددة -
Pool and
dummy.Pool - لكن له واجهة ودلالات مختلفة تمامًا. هذه فئة أساسية غير مخصصة للتنفيذ ولديها تطبيقان محددان:
- ThreadPoolExecutor : الذي يمثل تجمع مؤشر ترابط
- ProcessPoolExecutor : الذي يمثل تجمع العمليات
يقدم كل
منفذ ثلاث طرق:
- إرسال (fn ، * args ، ** kwargs) : يقوم بجدولة وظيفة fn للتنفيذ في تجمع الموارد وإرجاع كائن Future يمثل تنفيذ الكائن المدعو
- map (func ، * iterables ، timeout = None ، chunksize = 1) : يتم تنفيذ وظيفة func على التكرار على غرار المعالجة المتعددة. طريقة Pool.map ()
- إيقاف التشغيل (انتظر = صحيح) : يؤدي هذا إلى إيقاف تشغيل Executor وتحرير جميع موارده.
الطريقة الأكثر إثارة للاهتمام هي
تقديم () بسبب كائن المستقبل الذي ترجع. إنه يمثل التنفيذ غير المتزامن لما يسمى ويمثل النتيجة بشكل غير مباشر. للحصول على قيمة الإرجاع الفعلية للكائن المرسل المرسَل ، يجب عليك استدعاء الأسلوب
Future.result () . وإذا تم إكمال الكائن الذي تم استدعاؤه بالفعل ، فلن تحظره طريقة
() وإرجاع ناتج الوظيفة. إذا لم يكن الأمر كذلك ، فسوف يقوم بحظره حتى تصبح النتيجة جاهزة. فكر في الأمر على أنه وعد بالنتيجة (إنه في الواقع نفس مفهوم الوعد في JavaScript). لا تحتاج إلى فكها مباشرة بعد استلامها (باستخدام طريقة
النتيجة () ) ، ولكن إذا حاولت القيام بذلك ، فمن المضمون في النهاية إرجاع شيء ما:
>>> def loudy_return(): ... print("processing") ... return 42 ... >>> from concurrent.futures import ThreadPoolExecutor >>> with ThreadPoolExecutor(1) as executor: ... future = executor.submit(loudy_return) ... processing >>> future <Future at 0x33cbf98 state=finished returned int> >>> future.result() 42
إذا كنت تريد استخدام الأسلوب
Executor.map () ، فهو لا يختلف في الاستخدام عن طريقة
Pool.map () للفئة
Pool من الوحدة النمطية متعددة المعالجات:
def main(): with ThreadPoolExecutor(POOL_SIZE) as pool: results = pool.map(fetch_place, PLACES) for result in results: present_result(result)
باستخدام Executor في حلقة حدث
مثيلات فئة Future التي يتم إرجاعها بواسطة أسلوب
Executor.submit () قريبة من الناحية النظرية جدًا من coroutines المستخدمة في البرمجة غير المتزامنة. لهذا السبب يمكننا استخدام الفنانين لإنشاء مزيج بين تعدد المهام التعاونية والمعالجة المتعددة أو تعدد العمليات.
جوهر هذا الحل البديل هو أسلوب
BaseEventLoop.run_in_executor (المنفذ ، func ، * args) لفئة حلقة
الحدث . هذا يسمح لك بالتخطيط لتنفيذ وظيفة func في عملية أو تجمع مؤشرات ترابط ممثلة بواسطة وسيطة المنفذ. الشيء الأكثر أهمية في هذه الطريقة هو أنها تُرجع الكائن المتوقع الجديد (الكائن الذي يمكن توقعه باستخدام عامل انتظار الانتظار). وبالتالي ، بفضل هذا ، يمكنك تنفيذ وظيفة حظر ليست coroutine تمامًا مثل coroutine ، ولن يتم حظرها ، بغض النظر عن المدة التي يستغرقها الانتهاء. سوف يتوقف فقط عن الوظيفة التي تتوقع نتائج من مثل هذه الدعوة ، لكن دورة الأحداث بأكملها ستستمر.
والحقيقة المفيدة هي أنك لست بحاجة حتى إلى إنشاء نسخة من المنفذ الخاص بك. إذا قمت بتمرير
None كوسيطة
للمنفذ ، فسيتم استخدام فئة
ThreadPoolExecutor مع العدد الافتراضي
لمؤشرات الترابط (بالنسبة لبيثون 3.5 ، هذا هو عدد المعالجات مضروبة في 5).
لذلك ، دعنا نفترض أننا لم نرغب في إعادة كتابة الجزء الإشكالي من حزمة python-gmaps التي كانت تسبب الصداع لدينا. يمكننا بسهولة تأجيل مكالمة حظر إلى سلسلة
رسائل منفصلة عن طريق الاتصال بـ
loop.run_in_executor () ، مع ترك وظيفة fetch_place () باعتبارها coroutine المتوقعة:
async def fetch_place(place): coro = loop.run_in_executor(None, api.geocode, place) result = await coro return result[0]
مثل هذا الحل أسوأ من امتلاك مكتبة غير متزامنة تمامًا للقيام بالمهمة ، لكنك تعلم أن هناك شيئًا على الأقل أفضل من لا شيء.بعد توضيح ما هو التزامن حقًا ، اتخذنا إجراءً وحللنا إحدى المشكلات المتوازية النموذجية باستخدام تعدد العمليات. بعد تحديد أوجه القصور الرئيسية في الكود لدينا وتصحيحها ، تحولنا إلى المعالجة المتعددة لنرى كيف ستعمل في حالتنا.بعد ذلك ، وجدنا أنه باستخدام وحدة متعددة المعالجات ، فإن استخدام العديد من العمليات أسهل بكثير من مؤشرات الترابط الأساسية ذات مؤشرات الترابط المتعددة. ولكن بعد ذلك فقط أدركنا أنه يمكننا استخدام واجهة برمجة التطبيقات (API) نفسها مع مؤشرات الترابط ، وذلك بفضل multiprocessing.dummy. وبالتالي ، فإن الاختيار بين المعالجة المتعددة وتعدد مؤشرات الترابط الآن يعتمد فقط على الحل الأنسب للمشكلة ، وليس الحل الذي لديه أفضل واجهة.عند الحديث عن تفصيل المشكلة ، جربنا أخيرًا البرمجة غير المتزامنة ، والتي ينبغي أن تكون أفضل حل للتطبيقات المتعلقة بالإدخال / الإخراج ، لفهم أننا لا نستطيع أن ننسى الخيوط والعمليات تمامًا. لذلك صنعنا دائرة ، عدنا إلى حيث بدأنا!وهذا يقودنا إلى الاستنتاج النهائي لهذا الفصل. لا يوجد حل يناسب الجميع. هناك عدة طرق قد تفضلها أو تعجبك أكثر. هناك بعض الطرق المناسبة لهذه المجموعة من المشكلات ، لكن عليك أن تعرف كل هذه المشاكل لتكون ناجحًا. في سيناريوهات واقعية ، يمكنك استخدام ترسانة كاملة من الأدوات وأنماط التوازي في تطبيق واحد ، وهذا ليس من غير المألوف.الاستنتاج السابق هو مقدمة ممتازة لموضوع الفصل التالي ، الفصل 14 "أنماط التصميم المفيدة". لأنه لا يوجد قالب واحد من شأنه حل جميع مشاكلك. يجب أن تعرف أكبر قدر ممكن ، لأنه في النهاية ستستخدمها كل يوم.