الثلاثي - البرمجة غير المتزامنة للناس

صورة

تحتوي Python على مكتبة ثلاثية - مكتبة برمجة غير متزامنة.
سيكون التعرف على Trio مثيرًا للاهتمام في الغالب لأولئك الذين يعملون على Asyncio ، لأنه بديل جيد يسمح لك بحل بعض المشاكل التي لا يمكن لـ Asyncio التعامل معها. في هذه المراجعة ، سننظر في ماهية Trio وما هي الميزات التي يمنحنا إياها.

بالنسبة لأولئك الذين بدأوا للتو العمل في البرمجة غير المتزامنة ، أقترح قراءة مقدمة صغيرة حول ما هو التزامن والتزامن.

التزامن وعدم التزامن


في البرمجة المتزامنة ، يتم تنفيذ جميع العمليات بالتسلسل ، ولا يمكنك بدء مهمة جديدة إلا بعد إكمال المهمة السابقة. ومع ذلك ، فإن إحدى نقاط "الألم" هي أنه إذا كان أحد سلاسل الرسائل يعمل على مهمة لفترة طويلة جدًا ، فقد يتجمد البرنامج بأكمله. هناك مهام لا تتطلب قوة حوسبة ، ولكنها تستغرق وقت المعالج ، والتي يمكن استخدامها بشكل أكثر عقلانية من خلال التحكم في سلسلة رسائل أخرى. في البرمجة المتزامنة ، لا توجد طريقة لإيقاف المهمة الحالية مؤقتًا لإكمال ما يلي في فجوته.

ما هو التزامن؟؟؟؟ من الضروري التمييز بين التزامن الحقيقي وعدم التزامن بين المدخلات والمخرجات. في حالتنا ، نحن نتحدث عن المدخلات والمخرجات غير المتزامنة. على الصعيد العالمي - من أجل توفير الوقت واستخدام مرافق الإنتاج بشكل أكثر كفاءة. يسمح لك عدم التزامن بتجاوز نواحي مشكلة سلاسل العمليات. في المدخلات غير المتزامنة ، لن ينتظر مؤشر الترابط الحالي تنفيذ بعض الأحداث الخارجية ، ولكنه سيتيح التحكم في مؤشر ترابط آخر. وبالتالي ، في الواقع ، يتم تنفيذ مؤشر ترابط واحد فقط في كل مرة. مؤشر الترابط الذي أعطى التحكم يذهب إلى قائمة الانتظار وينتظر لعودة التحكم إليه. ربما ، بحلول ذلك الوقت ، سيحدث الحدث الخارجي المتوقع وسيكون من الممكن مواصلة العمل. سيسمح لك هذا بالتبديل بين المهام لتقليل إضاعة الوقت.

والآن يمكننا العودة إلى ما هوAsyncio . تشغيل حلقة حدث المكتبة هذه ( حلقة الحدث) ، والتي تتضمن قائمة انتظار المهام والحلقة نفسها. تتحكم الدورة في تنفيذ المهام ، أي أنها تستمد المهام من قائمة الانتظار وتحدد ما سيحدث لها. على سبيل المثال ، قد يكون معالجة مهام الإدخال / الإخراج. أي أن حلقة الحدث تحدد المهمة وتسجيلها وفي الوقت المناسب تبدأ معالجتها.

Coroutines هي وظائف خاصة تعيد التحكم في هذه المهمة إلى حلقة الأحداث ، أي تعيدها إلى قائمة الانتظار. من الضروري إطلاق هذه الفصائل بدقة من خلال سلسلة من الأحداث.

أيضا هناك العقود الآجلة- الأشياء التي يتم فيها تخزين النتيجة الحالية لتنفيذ المهمة. قد تكون هذه معلومات تفيد بأن المهمة لم تتم معالجتها بعد أو تم الحصول على النتيجة بالفعل ؛ أو قد يكون هناك استثناء.

بشكل عام ، مكتبة Asyncio معروفة جيدًا ، ولكن لديها عدد من السلبيات التي يمكن لـ Trio إغلاقها.

ثلاثي


وفقًا لمؤلف المكتبة ، Nathaniel Smith ، عند تطوير Trio ، سعى إلى إنشاء أداة خفيفة الوزن وسهلة الاستخدام للمطور ، والتي ستوفر أبسط إدخال / إخراج غير متزامن ومعالجة الأخطاء.

ميزة مهمة في Trio هي إدارة السياق غير المتزامن ، التي لا تمتلكها Asyncio. للقيام بذلك ، أنشأ المؤلف في تريو ما يسمى "الحضانة"(حضانة) - منطقة الإلغاء التي تتحمل المسؤولية عن الذرية (استمرارية) مجموعة من الخيوط. الفكرة الرئيسية هي أنه في حالة فشل أحد الكورونات في "الحضانة" ، فسيتم إكمال جميع التدفقات في "الحضانة" أو إلغاؤها بنجاح. في أي حال ، ستكون النتيجة صحيحة. وفقط عند اكتمال جميع coroutines ، بعد الخروج من الوظيفة ، يقرر المطور نفسه كيفية المتابعة.

أي أن "الأطفال" يسمح لك بمنع استمرار معالجة الأخطاء ، مما قد يؤدي إلى حقيقة أن "كل شيء" سوف "يسقط" أو أن النتيجة ستكون نتيجة غير صحيحة.
هذا هو بالضبط ما يمكن أن يحدث مع Asyncio ، لأنه في Asyncio لا تتوقف العملية ، على الرغم من حقيقة حدوث خطأ. وفي هذه الحالة ، أولاً ، لن يعرف المطور ما حدث بالضبط في وقت الخطأ ، وثانيًا ، ستستمر المعالجة.

أمثلة


خذ بعين الاعتبار أبسط مثال على ميزتين متنافستين:

Asyncio

import asyncio

async def foo1():
    print('  foo1: ')
    await asyncio.sleep(2)
    print('  foo1: ')

async def foo2():
    print('  foo2: ')
    await asyncio.sleep(1)
    print('  foo2: ')

loop = asyncio.get_event_loop()
bundle = asyncio.wait([
    loop.create_task(foo1()),
    loop.create_task(foo2()),
])
try:
    loop.run_until_complete(bundle)
finally:
    loop.close()

ثلاثي

import trio

async def foo1():
    print('  foo1: ')
    await trio.sleep(2)
    print('  foo1: ')

async def foo2():
    print('  foo2: ')
    await trio.sleep(1)
    print('  foo2: ')

async def root():
    async with trio.open_nursery() as nursery:
        nursery.start_soon(foo1)
        nursery.start_soon(foo2)

trio.run(root)

في كلتا الحالتين ستكون النتيجة هي نفسها:

foo1: 
foo2: 
foo2: 
foo1: 

من الناحية الهيكلية ، فإن كود Asyncio و Trio في هذا المثال مشابه.

الفرق الواضح هو أن Trio لا يتطلب إكمالًا صريحًا لحلقة الحدث.

فكر في مثال أكثر حيوية قليلاً. دعونا نتصل بخدمة الويب للحصول على طابع زمني.

بالنسبة إلى Asyncio ، سنستخدم أيضًا aiohttp :

import time
import asyncio
import aiohttp

URL = 'https://yandex.ru/time/sync.json?geo=213'
MAX_CLIENTS = 5

async def foo(session, i):
    start = time.time()
    async with session.get(URL) as response:
        content = await response.json()
        print(f'{i} | {content.get("time")} (  {time.time() - start})')

async def root():
    start = time.time()
    async with aiohttp.ClientSession() as session:
        tasks = [
            asyncio.ensure_future(foo(session, i))
            for i in range(MAX_CLIENTS)
        ]
        await asyncio.wait(tasks)
    print(f'  {time.time() - start}')

ioloop = asyncio.get_event_loop()
try:
    ioloop.run_until_complete(root())
finally:
    ioloop.close()

بالنسبة لـ Trio نستخدم الطلبات :

import trio
import time
import asks
URL = 'https://yandex.ru/time/sync.json?geo=213'
MAX_CLIENTS = 5

asks.init('trio')

async def foo(i):
    start = time.time()
    response = await asks.get(URL)
    content = response.json()
    print(f'{i} | {content.get("time")} (  {time.time() - start})')

async def root():
    start = time.time()
    async with trio.open_nursery() as nursery:
        for i in range(MAX_CLIENTS):
            nursery.start_soon(foo, i)

    print(f'  {time.time() - start}')

trio.run(root)

في كلتا الحالتين ، نحصل على شيء مثل

0 | 1543837647522 (  0.11855053901672363)
2 | 1543837647535 (  0.1389765739440918)
3 | 1543837647527 (  0.13904547691345215)
4 | 1543837647557 (  0.1591191291809082)
1 | 1543837647607 (  0.2100353240966797)
  0.2102828025817871

حسن. تخيل أنه أثناء تنفيذ أحد الكورتيونات ، حدث خطأ
في Asyncio.

async def foo(session, i):
    start = time.time()
    if i == 3:
        raise Exception
    async with session.get(URL) as response:
        content = await response.json()
        print(f'{i} | {content.get("time")} (  {time.time() - start})')

1 | 1543839060815 (  0.10857725143432617)
2 | 1543839060844 (  0.10372781753540039)
5 | 1543839060843 (  0.10734415054321289)
4 | 1543839060874 (  0.13985681533813477)
  0.15044045448303223
Traceback (most recent call last):
  File "...py", line 12, in foo
    raise Exception
Exception

للثلاثي

async def foo(i):
    start = time.time()
    response = await asks.get(URL)
    content = response.json()
    if i == 3:
        raise Exception
    print(f'{i} | {content.get("time")} (  {time.time() - start})')


4 | 1543839223372 (  0.13524699211120605)
2 | 1543839223379 (  0.13848185539245605)
Traceback (most recent call last):
  File "...py", line 28, in <module>
    trio.run(root)
  File "/lib64/python3.6/site-packages/trio/_core/_run.py", line 1337, in run
    raise runner.main_task_outcome.error
  File "...py", line 23, in root
    nursery.start_soon(foo, i)
  File "/lib64/python3.6/site-packages/trio/_core/_run.py", line 397, in __aexit__
    raise combined_error_from_nursery
  File "...py", line 15, in foo
    raise Exception
Exception

من الواضح أنه في Trio ، فور حدوث الخطأ ، نجحت "منطقة الإلغاء" ، وتم إنهاء وظيفتين من المهام الأربعة التي لم تحتوي على أخطاء بشكل غير طبيعي.

في Asyncio ، تم الانتهاء من جميع المهام ، وعندها فقط ظهر المسار.

في هذا المثال ، هذا ليس مهمًا ، ولكن دعونا نتخيل أن المهام بطريقة أو بأخرى تعتمد على بعضها البعض ، ويجب أن يكون لمجموعة المهام خاصية الذرية. في هذه الحالة ، يصبح الرد على الخطأ في الوقت المناسب أكثر أهمية. بالطبع ، يمكنك استخدام انتظار asyncio.wait (المهام ، return_when = FIRST_EXCEPTION) ، ولكن يجب أن تتذكر إكمال المهام المفتوحة بشكل صحيح.

فيما يلي مثال آخر:

لنفترض أن coroutines تصل في وقت واحد إلى العديد من خدمات الويب المماثلة ، وأن الاستجابة الأولى التي تم تلقيها مهمة.

import asyncio
from asyncio import FIRST_COMPLETED
import aiohttp

URL = 'https://yandex.ru/time/sync.json?geo=213'
MAX_CLIENTS = 5

async def foo(session):
    async with session.get(URL) as response:
        content = await response.json()
        return content.get("time")

async def root():
    async with aiohttp.ClientSession() as session:
        tasks = [
            asyncio.ensure_future(foo(session))
            for i in range(1, MAX_CLIENTS + 1)
        ]
        done, pending = await asyncio.wait(tasks, return_when=FIRST_COMPLETED)
        print(done.pop().result())
        for future in pending:
            future.cancel()

ioloop = asyncio.get_event_loop()
try:
    ioloop.run_until_complete(root())
except:
    ioloop.close()

كل شيء بسيط للغاية. الشرط الوحيد هو أن تتذكر إكمال المهام التي لم تكتمل.

في Trio ، يكون تحريك مناورة مماثلة أصعب قليلاً ، ولكن يكاد يكون من المستحيل ترك "ذيول" غير مرئية على الفور:

import trio
import asks
URL = 'https://yandex.ru/time/sync.json?geo=213'
MAX_CLIENTS = 5
asks.init('trio')

async def foo(session, send_channel, nursery):
    response = await session.request('GET', url=URL)
    content = response.json()
    async with send_channel:
        send_channel.send_nowait(content.get("time"))
    nursery.cancel_scope.cancel()

async def root():
    send_channel, receive_channel = trio.open_memory_channel(1)
    async with send_channel, receive_channel:
        async with trio.open_nursery() as nursery:
            async with asks.Session() as session:
                for i in range(MAX_CLIENTS):
                    nursery.start_soon(foo, session, send_channel.clone(), nursery)

        async with receive_channel:
            x = await receive_channel.receive()
            print(x)

trio.run(root)

nursery.cancel_scope.cancel () - أول كوروتين مكتمل سيستدعي وظيفة في منطقة التراجع التي ستلغي جميع المهام الأخرى ، لذلك لا داعي للقلق بشأنها بشكل منفصل.
صحيح ، من أجل نقل نتيجة تنفيذ coroutine إلى الوظيفة التي تسببت فيها ، سيتعين عليك بدء قناة اتصال.

نأمل أن تكون هذه المراجعة المقارنة قد وفرت الميزات الرئيسية لـ Trio. شكرا للجميع!

All Articles