تحتوي Python على مكتبة ثلاثية - مكتبة برمجة غير متزامنة.سيكون التعرف على Trio مثيرًا للاهتمام في الغالب لأولئك الذين يعملون على Asyncio ، لأنه بديل جيد يسمح لك بحل بعض المشاكل التي لا يمكن لـ Asyncio التعامل معها. في هذه المراجعة ، سننظر في ماهية Trio وما هي الميزات التي يمنحنا إياها.بالنسبة لأولئك الذين بدأوا للتو العمل في البرمجة غير المتزامنة ، أقترح قراءة مقدمة صغيرة حول ما هو التزامن والتزامن.التزامن وعدم التزامن
في البرمجة المتزامنة ، يتم تنفيذ جميع العمليات بالتسلسل ، ولا يمكنك بدء مهمة جديدة إلا بعد إكمال المهمة السابقة. ومع ذلك ، فإن إحدى نقاط "الألم" هي أنه إذا كان أحد سلاسل الرسائل يعمل على مهمة لفترة طويلة جدًا ، فقد يتجمد البرنامج بأكمله. هناك مهام لا تتطلب قوة حوسبة ، ولكنها تستغرق وقت المعالج ، والتي يمكن استخدامها بشكل أكثر عقلانية من خلال التحكم في سلسلة رسائل أخرى. في البرمجة المتزامنة ، لا توجد طريقة لإيقاف المهمة الحالية مؤقتًا لإكمال ما يلي في فجوته.ما هو التزامن؟؟؟؟ من الضروري التمييز بين التزامن الحقيقي وعدم التزامن بين المدخلات والمخرجات. في حالتنا ، نحن نتحدث عن المدخلات والمخرجات غير المتزامنة. على الصعيد العالمي - من أجل توفير الوقت واستخدام مرافق الإنتاج بشكل أكثر كفاءة. يسمح لك عدم التزامن بتجاوز نواحي مشكلة سلاسل العمليات. في المدخلات غير المتزامنة ، لن ينتظر مؤشر الترابط الحالي تنفيذ بعض الأحداث الخارجية ، ولكنه سيتيح التحكم في مؤشر ترابط آخر. وبالتالي ، في الواقع ، يتم تنفيذ مؤشر ترابط واحد فقط في كل مرة. مؤشر الترابط الذي أعطى التحكم يذهب إلى قائمة الانتظار وينتظر لعودة التحكم إليه. ربما ، بحلول ذلك الوقت ، سيحدث الحدث الخارجي المتوقع وسيكون من الممكن مواصلة العمل. سيسمح لك هذا بالتبديل بين المهام لتقليل إضاعة الوقت.والآن يمكننا العودة إلى ما هوAsyncio . تشغيل حلقة حدث المكتبة هذه ( حلقة الحدث) ، والتي تتضمن قائمة انتظار المهام والحلقة نفسها. تتحكم الدورة في تنفيذ المهام ، أي أنها تستمد المهام من قائمة الانتظار وتحدد ما سيحدث لها. على سبيل المثال ، قد يكون معالجة مهام الإدخال / الإخراج. أي أن حلقة الحدث تحدد المهمة وتسجيلها وفي الوقت المناسب تبدأ معالجتها.Coroutines هي وظائف خاصة تعيد التحكم في هذه المهمة إلى حلقة الأحداث ، أي تعيدها إلى قائمة الانتظار. من الضروري إطلاق هذه الفصائل بدقة من خلال سلسلة من الأحداث.أيضا هناك العقود الآجلة- الأشياء التي يتم فيها تخزين النتيجة الحالية لتنفيذ المهمة. قد تكون هذه معلومات تفيد بأن المهمة لم تتم معالجتها بعد أو تم الحصول على النتيجة بالفعل ؛ أو قد يكون هناك استثناء.بشكل عام ، مكتبة Asyncio معروفة جيدًا ، ولكن لديها عدد من السلبيات التي يمكن لـ Trio إغلاقها.ثلاثي
وفقًا لمؤلف المكتبة ، Nathaniel Smith ، عند تطوير Trio ، سعى إلى إنشاء أداة خفيفة الوزن وسهلة الاستخدام للمطور ، والتي ستوفر أبسط إدخال / إخراج غير متزامن ومعالجة الأخطاء.ميزة مهمة في Trio هي إدارة السياق غير المتزامن ، التي لا تمتلكها Asyncio. للقيام بذلك ، أنشأ المؤلف في تريو ما يسمى "الحضانة"(حضانة) - منطقة الإلغاء التي تتحمل المسؤولية عن الذرية (استمرارية) مجموعة من الخيوط. الفكرة الرئيسية هي أنه في حالة فشل أحد الكورونات في "الحضانة" ، فسيتم إكمال جميع التدفقات في "الحضانة" أو إلغاؤها بنجاح. في أي حال ، ستكون النتيجة صحيحة. وفقط عند اكتمال جميع coroutines ، بعد الخروج من الوظيفة ، يقرر المطور نفسه كيفية المتابعة.أي أن "الأطفال" يسمح لك بمنع استمرار معالجة الأخطاء ، مما قد يؤدي إلى حقيقة أن "كل شيء" سوف "يسقط" أو أن النتيجة ستكون نتيجة غير صحيحة.هذا هو بالضبط ما يمكن أن يحدث مع Asyncio ، لأنه في Asyncio لا تتوقف العملية ، على الرغم من حقيقة حدوث خطأ. وفي هذه الحالة ، أولاً ، لن يعرف المطور ما حدث بالضبط في وقت الخطأ ، وثانيًا ، ستستمر المعالجة.أمثلة
خذ بعين الاعتبار أبسط مثال على ميزتين متنافستين:Asyncioimport 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. شكرا للجميع!