התחל לעבוד עם אסינכרון בפייתון

תכנות אסינכרוני, או בקיצור אסינכרון , הוא תכונה של שפות מודרניות רבות המאפשרת לתוכנית ללהטט בפעולות מרובות מבלי לחכות או לנתק את אחת מהן. זו דרך חכמה להתמודד ביעילות עם משימות כמו רשת או קלט / פלט של קבצים, שם רוב הזמן של התוכנית מושקע בהמתנה למשימה שתסתיים.

שקול יישום גרידה באינטרנט הפותח 100 חיבורי רשת. אתה יכול לפתוח חיבור אחד, לחכות לתוצאות, ואז לפתוח את הבא ולחכות לתוצאות וכן הלאה. רוב הזמן שמופעלת בתוכנית מושקע בהמתנה לתגובת רשת, ולא בעבודה ממשית.

Async נותן לך שיטה יעילה יותר: פתח את כל 100 החיבורים בבת אחת, ואז החלף בין כל חיבור פעיל כאשר הם מחזירים תוצאות. אם חיבור אחד לא מחזיר תוצאות, עבור לחבר הבא, וכן הלאה, עד שכל החיבורים החזירו את הנתונים שלהם.

תחביר אסינכרון הוא כעת מאפיין סטנדרטי בפייתון, אך פיתוניסטות ותיקות שרגילות לעשות דבר אחד בכל פעם עשויות להתקשות לכרוך את ראשן סביבו. במאמר זה נחקור כיצד פועל תכנות אסינכרוני בפייתון וכיצד להשתמש בו.

שים לב שאם אתה רוצה להשתמש ב- async ב- Python, עדיף להשתמש ב- Python 3.7 או Python 3.8 (הגרסה האחרונה נכון לכתיבת שורות אלה). נשתמש בתחביר האסינכרון של פייתון ובפונקציות העוזר כפי שהוגדרו באותן גרסאות השפה.

מתי להשתמש בתכנות אסינכרוני

באופן כללי, הזמנים הטובים ביותר לשימוש ב- async הם כאשר אתה מנסה לבצע עבודה עם התכונות הבאות:

  • העבודה נמשכת זמן רב להשלמתה.
  • העיכוב כולל המתנה לפעולות קלט / פלט (דיסק או רשת), ולא חישוב.
  • העבודה כוללת פעולות קלט / פלט רבות המתרחשות בבת אחת, או פעולות קלט / פלט אחת או יותר שקורות כאשר אתה מנסה לבצע משימות אחרות.

Async מאפשר לך להגדיר מספר משימות במקביל ולאתר באמצעותן ביעילות, מבלי לחסום את שאר היישומים שלך.

כמה דוגמאות למשימות שעובדות היטב עם async:

  • גרידה באינטרנט, כמתואר לעיל.
  • שירותי רשת (למשל שרת אינטרנט או מסגרת).
  • תוכניות המתאמות תוצאות ממספר מקורות שלוקח זמן רב להחזרת ערכים (למשל, שאילתות מסדי נתונים בו זמנית).

חשוב לציין שתכנות אסינכרוני שונה מריבוי הליכי משנה או ריבוי עיבוד. פעולות Async כולן פועלות באותו אשכול, אך הן נכנעות זו לזו לפי הצורך, מה שהופך את async ליעיל יותר מאשר השחלה או עיבוד רב עבור סוגים רבים של משימות. (עוד על כך בהמשך.)

פייתון asyncawaitוasyncio

Python שנוסף לאחרונה שתי מילות מפתח, asyncוכן await, ליצירת פעולות async. שקול סקריפט זה:

def get_server_status (server_addr) # פעולה שעשויה להיות ארוכה ... להחזיר server_status def server_ops () results = [] results.append (get_server_status ('addr1.server') results.append (get_server_status ('addr2.server') return תוצאות 

גרסת אסינכרון של אותו סקריפט - לא פונקציונלית, מספיק כדי לתת לנו מושג על אופן פעולת התחביר - עשויה להיראות כך.

async def get_server_status (server_addr) # פעולה שעשויה להיות ארוכה ... להחזיר server_status async def server_ops () results = [] results.append (await get_server_status ('addr1.server') results.append (await get_server_status ('addr2. שרת ') להחזיר תוצאות 

פונקציות שמקודמות asyncלמילת המפתח הופכות לפונקציות אסינכרוניות, המכונות גם קורוטינות . קורוטינים מתנהגים באופן שונה מפונקציות רגילות:

  • Coroutines יכולים להשתמש במילת מפתח אחרת, awaitהמאפשרת ל- coroutine להמתין לתוצאות מ coroutine אחר מבלי לחסום. עד שהתוצאות יחזרו מ- awaitcoroutine ed, Python עובר בחופשיות בין coroutines אחרים פועלים.
  • Coroutines ניתן להתקשר רקasync מפונקציות אחרות . אם אתה מריץ server_ops()או get_server_status()כפי שהוא מהגוף של התסריט, לא תקבל את התוצאות שלהם; תקבל אובייקט קוראוטינה של פייתון, שלא ניתן להשתמש בו ישירות.

אז אם איננו יכולים לקרוא asyncלפונקציות מפונקציות שאינן סינכרוניות ואיננו יכולים להפעיל asyncפונקציות ישירות, כיצד נשתמש בהן? תשובה: באמצעות asyncioהספרייה, המגשרת asyncושאר פיתון.

Python asyncawaitו- asyncioלדוגמה

הנה דוגמה (שוב, לא פונקציונלית אך ממחישה) לאופן שבו ניתן לכתוב יישום גירוד באינטרנט באמצעות asyncו- asyncio. סקריפט זה לוקח רשימה של כתובות אתרים ומשתמש במופעים מרובים של asyncפונקציה מספרייה חיצונית ( read_from_site_async()) כדי להוריד אותם ולצבור את התוצאות.

ייבא asyncio מ- scraping_library ייבוא ​​read_from_site_async async def main (url_list): return await asyncio.gather (* [read_from_site_async (_) for _ in url_list]) urls = ['//site1.com','//othersite.com', '//newsite.com'] תוצאות = asyncio.run (ראשי (urls)) הדפס (תוצאות) 

בדוגמה שלעיל אנו משתמשים בשתי asyncioפונקציות נפוצות :

  • asyncio.run()משמש להפעלת asyncפונקציה מהחלק הלא אסינכרוני של הקוד שלנו, וכך לבעוט את כל פעילויות האסינכרון של הפרוגרם. (ככה אנחנו רצים main()).
  • asyncio.gather()לוקח פונקציה אחת או יותר מקושטות בסינכרון (במקרה זה, מספר מקרים read_from_site_async()מתוך ספריית הגרידה ברשת ההיפותטית שלנו), מפעילה את כולן ומחכה שכל התוצאות יגיעו.

הרעיון כאן הוא, אנו מתחילים את פעולת הקריאה של כל האתרים בבת אחת, ואז אוספים את התוצאות כשהן מגיעות (ומכאן asyncio.gather()). אנחנו לא מחכים לביצוע פעולה אחת לפני שנעבור לפעולה הבאה.

רכיבי אפליקציות אסינכרון של Python

כבר הזכרנו כיצד אפליקציות אסינק של Python משתמשות ב- coroutines כמרכיב העיקרי שלהן, תוך שאיבה על asyncioהספריה להפעלתן. כמה אלמנטים אחרים הם גם המפתח ליישומים אסינכרוניים בפייתון:

לולאות אירועים

asyncioהספרייה יוצרת ומנהלת לולאות אירוע , המנגנונים שפועלים coroutines עד שהם מסיימים. רק לולאת אירועים אחת אמורה לפעול בכל פעם בתהליך פייתון, ולו רק כדי להקל על המתכנת לעקוב אחר מה שנכנס לתוכו.

משימות

כאשר אתה מגיש קורוטין לולאת אירועים לעיבוד, אתה יכול להחזיר Taskאובייקט, המספק דרך לשלוט בהתנהגות קורוטין מחוץ לולאת האירוע. אם אתה צריך לבטל את המשימה הפועלת, למשל, אתה יכול לעשות זאת על ידי קריאה .cancel()לשיטת המשימה .

הנה גרסה מעט שונה של סקריפט מגרד האתר המציג את לולאת האירוע והמשימות בעבודה:

ייבא asyncio מ- scraping_library ייבוא ​​read_from_site_async משימות = [] async def ראשי (url_list): עבור n ב- url_list: משימות.תוספות (asyncio.create_task (read_from_site_async (n))) הדפס (משימות) חזור לחכות asyncio.gather (* = ['//site1.com','//othersite.com','//newsite.com'] loop = asyncio.get_event_loop () results = loop.run_until_complete (main (urls)) הדפס (תוצאות) 

סקריפט זה משתמש בלולאת האירוע ובאובייקטים המשימה בצורה מפורשת יותר.

  • .get_event_loop()השיטה מספקת לנו אובייקט המאפשר לנו לשלוט על לולאת האירוע ישירות, על ידי הגשת פונקציות async אל באמצעות תכנותית .run_until_complete(). בסקריפט הקודם נוכל להפעיל רק פונקציית אסינכרון ברמה העליונה באמצעות asyncio.run(). אגב, .run_until_complete() עושה בדיוק את מה שכתוב: הוא מריץ את כל המשימות המסופקות עד לסיומן, ואז מחזיר את התוצאות באצווה אחת.
  • .create_task()השיטה לוקחת פונקציה להרצה, כולל הפרמטרים שלו, והוא נותן לנו בחזרה Taskאובייקט כדי להפעיל אותו. כאן אנו מגישים כל כתובת אתר בנפרד Taskללולאת האירועים, ומאחסנים את Taskהאובייקטים ברשימה. שים לב שאנחנו יכולים לעשות זאת רק בתוך לולאת האירוע - כלומר בתוך asyncפונקציה.

How much control you need over the event loop and its tasks will depend on how complex the application is that you’re building. If you just want to submit a set of fixed jobs to run concurrently, as with our web scraper, you won’t need a whole lot of control—just enough to launch jobs and gather the results. 

By contrast, if you’re creating a full-blown web framework, you’ll want far more control over the behavior of the coroutines and the event loop. For instance, you may need to shut down the event loop gracefully in the event of an application crash, or run tasks in a threadsafe manner if you’re calling the event loop from another thread.

Async vs. threading vs. multiprocessing

At this point you may be wondering, why use async instead of threads or multiprocessing, both of which have been long available in Python?

First, there is a key difference between async and threads or multiprocessing, even apart from how those things are implemented in Python. Async is about concurrency, while threads and multiprocessing are about parallelism. Concurrency involves dividing time efficiently among multiple tasks at once—e.g., checking your email while waiting for a register at the grocery store. Parallelism involves multiple agents processing multiple tasks side by side—e.g., having five separate registers open at the grocery store.

Most of the time, async is a good substitute for threading as threading is implemented in Python. This is because Python doesn’t use OS threads but its own cooperative threads, where only one thread is ever running at a time in the interpreter. In comparison to cooperative threads, async provides some key advantages:

  • Async functions are far more lightweight than threads. Tens of thousands of asynchronous operations running at once will have far less overhead than tens of thousands of threads.
  • The structure of async code makes it easier to reason about where tasks pick up and leave off. This means data races and thread safety are less of an issue. Because all tasks in the async event loop run in a single thread, it’s easier for Python (and the developer) to serialize how they access objects in memory.
  • Async operations can be cancelled and manipulated more readily than threads. The Task object we get back from asyncio.create_task() provides us with a handy way to do this.

Multiprocessing in Python, on the other hand, is best for jobs that are heavily CPU-bound rather than I/O-bound. Async actually works hand-in-hand with multiprocessing, as you can use asyncio.run_in_executor() to delegate CPU-intensive jobs to a process pool from a central process, without blocking that central process.

Next steps with Python async

The best first thing to do is build a few, simple async apps of your own. Good examples abound now that asynchronous programming in Python has undergone a few versions and had a couple of years to settle down and become more widely used. The official documentation for asyncio is worth reading over to see what it offers, even if you don’t plan to make use of all of its functions.

אתה יכול גם לחקור את המספר ההולך וגדל של ספריות ותוכנות תווך המופעלות על-ידי async, שרבות מהן מספקות גרסאות אסינכרוניות ולא חסימות של מחברי מסדי נתונים, פרוטוקולי רשת וכדומה. aio-libsיש מאגר כמה כאלה מפתח, כגון aiohittpהספרייה עבור גישה לאינטרנט. כדאי גם לחפש בספריות עם אינדקס חבילות ה- Python עם asyncמילת המפתח. עם משהו כמו תכנות אסינכרוני, הדרך הטובה ביותר ללמוד היא לראות כיצד אחרים השתמשו בו לשימוש.