קוד אסינכרוני בפייתון באמצעות ספריית asyncio
פייתון משתמש בשתי גישות להרצת קוד code execution. הגישה הסינכרונית בה הקוד מבוצע שורה אחרי שורה כאשר הגישה החלופית מאפשרת לכמה משימות להתבצע בו-זמנית concurrent מה שיכול לקצר את משך הביצוע. הגישה הא-סינכרונית עובדת עם אותו ה- thread אבל קופצת בין ביצוע של כמה משימות במקום לחכות להשלמת כל פעולה בנפרד מה שמקצר משמעותית את זמן הביצוע. במדריך זה נלמד איך לכתוב קודא אסינכרוני באמצעות ספריית asyncio של פייתון.
עם פייתון זה מאוד קל לצרוך מידע שמקורו ברשת האינטרנט. לדוגמה, קריאה לשרת מרוחק api המספק טקסטים אקראיים, והדפסת התגובה למסך:
# Importing the requests library
import requests
URL = "https://official-joke-api.appspot.com/random_joke"
# Making a GET request to the specified URL to fetch a random text
response = requests.get(URL)
# Printing the response to the console
print(response.text)
התגובה היא טקסט אקראי בפורמט json. כך נראתה התגובה כשאני הרצתי את הקוד לעיל:
{"type":"programming","setup":"3 SQL statements walk into a NoSQL bar. Soon, they walk out","punchline":"They couldn't find a table.","id":376}נקרא לשירות המרוחק 10 פעמים, ונתזמן את משך הזמן שתארך הרצת הקוד:
# Importing the requests library
import requests, time
URL = "https://official-joke-api.appspot.com/random_joke"
# start the timer
s = time.perf_counter()
for _ in range(9):
# Making a GET request to the specified URL to fetch a random joke
response = requests.get(URL)
# Printing the response to the console
print(response.text)
elapsed = time.perf_counter() - s
print(f"Code execution took {elapsed:0.2f} seconds.")
התוצאה:
Code execution took 8.98 seconds.
נאמר שאתה צריך לקרוא לשירות זה 10 פעמים או 100 פעמים זה יוצר בעיה בגלל שהקוד עובד באופן סינכרוני, כלומר מבצע את הפקודות שורה אחר שורה, ועל כן עוצר block את ביצוע הקוד עד שמתקבלת תגובה מה-api. זה לא טוב לנו כי הזמן מתבזבז על לחכות לשירות חיצוני במקום שהתוכנה תמשיך לבצע את יתר הקוד אותו היא צריכה להריץ.
חלק גדול מזמן הריצה של תוכנה מבוזבז על לחכות. זה יכול להיות המתנה לפתיחה של מסמך לקריאה, תחקור מסד נתונים, ציפייה לתגובת המשתמש או הבאת מידע מרשת האינטרנט. הפעולות בהם צוואר הבקבוק הוא הזמן שצריך לחכות לתגובה קרויות בשם I/O-bound tasks - אילו משימות בהם התוכנה צריכה לחכות לפעולות Input Output. זה בניגוד לפעולות שהם CPU-bound בהם צוואר הבקבוק הוא יכולת המעבד לעשות חישובים. לדוגמה, עיבוד תמונה, הצפנה, סימולציות.
פייתון משתמש בשתי גישות להרצת קוד code execution. הגישה הסינכרונית בה הקוד מבוצע שורה אחרי שורה כאשר הגישה החלופית מאפשרת לכמה משימות להתבצע בו-זמנית concurrent מה שיכול לקצר את משך הביצוע. בין האפשרויות לביצוע קוד בו-זמנית אפשר למנות: multiprocessing המיועדת לפתור בעיות שהם CPU-bound ו-multithreading המיועדת לפתור בעיות כמו שראינו שהם I/O-bound אבל היישום שלה כרוך בבעיות רבות. יש גישה שלישית שהיא הגישה האסינכרונית שלא מבצעת פעולות במקביל (לא בתהליכים מקבילים וגם לא ב-threads שונים) אלא עובדת עם אותו ה- thread כאשר היא קופצת בין ביצוע של כמה משימות במקום לחכות להשלמת כל פעולה בנפרד.
בוא נראה דוגמה לתהליך אסינכרוני. אם המשימה היא לקרוא לשני api שונים, נקרא להם api1 ו-api2, בזמן שלאחר שיגור הבקשה ל-api1 בזמן שעובר עד לקבלת התשובה חזרה, הקוד האסינכרוני קופץ מביצוע המשימה מול api1 ועובר לשגר בקשה ל-api2, ועד שהשני משיב הקוד האסינכרוני קופץ חזרה לבדוק אם התשובה מ-api1 כבר הגיעה, ואם הגיעה לטפל בה, ואז חוזר לטפל ב-2.
הקפיצות בין ביצוע משימות שונות בזמן שמחכים לתגובה: של שרת מרוחק, משתמש אנושי, פתיחת קובץ במערכת וכיו"ב היא מה שיכול לחסוך את זמן ההמתנה של ביצוע הקוד וזה גם מה שמקצר את משך הביצוע של התוכנה, וזה מה שעושה הקוד האסינכרוני.
ספריית asyncio
ספריית asyncio מיועדת לביצוע קוד באופן אסינכורני. היא מספקת לולאה event loop שרצה בין קורוטינות coroutines אילו הפונקציות אותן היא צריכה לבצע. הספרייה משתמשת ב-thread יחיד כדי לבצע כמה משימות "במקביל" בדגש על משימות I/O-bound. דוגמת: צריכת משאבים באמצעות רשת האינטרנט או קריאת קבצים בלי שהם יחסמו את ביצוע הקוד. בשביל הביצוע היא משתמש ב-event loop אשר מחליף בין ביצוע המשימות tasks כאשר יש זמן המתנה בין זמן הפעלת המשימה לזמן סיום המשימה. התוצאה היא חסכון בזמן הביצוע.
Asyncio עובד עם קורוטינות coroutines שהם פונקציות מיוחדות שניתן להפסיק את פעולתם ולחדש לפי הצורך מה שמאפשר את הרצת הקוד באופן אסינכרוני. נגדיר פונקציה כקורוטינה על ידי כך שנשים לפניה את מילת המפתח async.
במילת המפתח await נשתמש בשביל לעצור את ביצוע הקורוטינה עד שמתקבלת תוצאה מקורוטינה אחרת.
השילוב async/await מאפשר כתיבת קוד אסינכרוני שיכול לטפל במשימות I/O-bound ביעילות.
את היישום האסינכרוני של הקוד נעשה באמצעות ספריית asyncio. נכתוב מחדש את הקוד הקורא לשרת המרוחק:
import asyncio
import aiohttp
URL = "https://official-joke-api.appspot.com/random_joke"
async def make_request():
"""
Coroutine that makes an HTTP GET request
and returns the response body as text.
"""
async with aiohttp.ClientSession() as session:
async with session.get(URL) as response:
return await response.text()
async def main():
"""
Main coroutine that calls make_request() and prints the response.
"""
response = await make_request()
print(response)
# Run the main coroutine using asyncio.run()
asyncio.run(main())
- ייבאנו את הספריות asyncio ו-aiohttp החיוניות להרצת הקוד באופן אסינכרוני. Asyncio היא הספרייה עליה מתבססת היכולת האסינכרונית של הקוד. Aiohttp מאפשרת לכתוב קוד שרת אסינכרוני ולצרוך קוד שרת גם כן באופן אסינכרוני.
- הפונקציה make_request משתמשת בספריית aiohttp לקרוא ל-URL המרוחק, ולקבל חזרה תגובה שהיא טקסט. הפונקציה היא קורוטינה ולכן היא מוגדרת באמצעות async. רק בתוך פונקציה המוגדרת async אנחנו רשאים להשתמש ב-await כדי לחכות לתגובה. בתוכה הגדרנו אובייקט ClientSession שהוא זה שאחראי על הפנייה ל-API באמצעות HTTP GET request. במקרה של הצלחה הקורוטינה מחזירה את טקסט התגובה. במקרה של כשלון, הקורוטינה מחזירה None.
- הפונקציה main קוראת לפונקציה make_request ומדפיסה את התגובה.
- asyncio.run משמש להרצת הקורוטינה הראשית.
שיפור הקוד האסינכרוני
נשפר את הקוד על ידי הוספת טיפול בשגיאות:
import asyncio
import aiohttp
URL = "https://official-joke-api.appspot.com/random_joke"
async def make_request():
"""
Coroutine that makes an HTTP GET request
and returns the response body as text.
"""
session = aiohttp.ClientSession()
try:
response = await session.get(URL)
response.raise_for_status()
return await response.text()
except aiohttp.ClientError as error:
print(f"Error occurred: {error}")
except aiohttp.http_exceptions.HttpProcessingError as error:
print(f"HTTP processing error occurred: {error}")
finally:
await session.close()
async def main():
"""
Main coroutine that calls make_request() and prints the response.
"""
response = await make_request()
if response:
print(response)
# Run the main coroutine using asyncio.run()
asyncio.run(main())
- הקוד מטפל בשגיאות דוגמאת שגיאות רשת או תגובה בלתי צפויה מהשרת המרוחק. במקרים של שגיאה, הוא ידפיס הודעה למסך.
- בתוך הבלוק finally אנחנו מוודאים שה-session ייסגר אפילו אם אירעה שגיאה.
תזמון משך הריצה של הקוד האסינכרוני
נתזמן את משך הריצה של הקוד האסינכרוני עבור 10 קריאות לשרת המרוחק תוך שימוש ב-asyncio:
import asyncio
import aiohttp
import time
URL = "https://official-joke-api.appspot.com/random_joke"
async def make_request():
"""
Coroutine that makes an HTTP GET request
and returns the response body as text.
"""
async with aiohttp.ClientSession() as session:
try:
response = await session.get(URL)
response.raise_for_status()
return await response.text()
except aiohttp.ClientError as error:
print(f"Error occurred: {error}")
except aiohttp.http_exceptions.HttpProcessingError as error:
print(f"HTTP processing error occurred: {error}")
async def main():
"""
Main coroutine that creates 10 tasks to call make_request() and wait for them to complete.
"""
tasks = [make_request() for _ in range(10)]
results = await asyncio.gather(*tasks)
for result in results:
if result:
print(result)
start_time = time.perf_counter()
# Run the main coroutine using asyncio.run()
asyncio.run(main())
elapsed_time = time.perf_counter() - start_time
print(f"Code execution took {elapsed_time:0.2f} seconds.")
התוצאה:
Code execution took 1.30 seconds.
במקום 9 שניות שארך ביצוע הקוד הסינכרוני, משך הביצוע של הקוד הא-סינכרוני התקצר ל-1.5 שניות. מדהים.
צריך לשים לב שבביצוע הקוד הפונקציה main יוצרת 10 משימות tasks, צוברת אותם באמצעות asyncio.gather ומחכה לסיום הביצוע של כולם (אם ננסה לעשות את אותו הדבר בתוך לולאה רגילה לא נרוויח דבר). אחרי סיום הביצוע של כל המשימות התוצאות מודפסות למסך אחת אחרי השנייה.
סיכום
בפייתון כשאנחנו אומרים שהקוד מתבצע באופן אסינכרוני אנחנו מתייחסים לזה שמספר משימות מתבצעות "בו-זמנית" על אותו ה-thread הודות לשימוש בקורוטינות coroutines, שהם פונקציות מיוחדות שניתן להתחיל ולהפסיק את פעולותם במהלך הביצוע, וכך לאפשר ביצוע של קורוטינות אחרות במקביל.
בשביל לבצע קוד אסינכרוני של פייתון אנחנו משתמשים בספריית asyncio הנעזרת ב-event loop לצורך תזמון של ביצוע קורוטינות. נשתמש בספריית asyncio כדי לייעל הרצת קוד שנתקע בגלל I/O דוגמת קריאת קובץ, או המתנה לתגובה של שרת מרוחק. מבחינת התחביר, נגדיר פונקציות אסינכרוניות באמצעות מילת המפתח async. בתוכה נגדיר חלקים של קוד שאנו מעוניינים לחכות להשלמת הביצוע שלהם באמצעות מילת המפתח await.
מדריכים נוספים בסדרה ללימוד פייתון
טיפול בחריגים בפייתון באמצעות הבלוקים try ו-except
Async/Await לסנכרון קוד של JavaScript
לכל המדריכים בסדרה ללימוד פייתון
אהבתם? לא אהבתם? דרגו!
0 הצבעות, ממוצע 0 מתוך 5 כוכבים
המדריכים באתר עוסקים בנושאי תכנות ופיתוח אישי. הקוד שמוצג משמש להדגמה ולצרכי לימוד. התוכן והקוד המוצגים באתר נבדקו בקפידה ונמצאו תקינים. אבל ייתכן ששימוש במערכות שונות, דוגמת דפדפן או מערכת הפעלה שונה ולאור השינויים הטכנולוגיים התכופים בעולם שבו אנו חיים יגרום לתוצאות שונות מהמצופה. בכל מקרה, אין בעל האתר נושא באחריות לכל שיבוש או שימוש לא אחראי בתכנים הלימודיים באתר.
למרות האמור לעיל, ומתוך רצון טוב, אם נתקלת בקשיים ביישום הקוד באתר מפאת מה שנראה לך כשגיאה או כחוסר עקביות נא להשאיר תגובה עם פירוט הבעיה באזור התגובות בתחתית המדריכים. זה יכול לעזור למשתמשים אחרים שנתקלו באותה בעיה ואם אני רואה שהבעיה עקרונית אני עשוי לערוך התאמה במדריך או להסיר אותו כדי להימנע מהטעיית הציבור.
שימו לב! הסקריפטים במדריכים מיועדים למטרות לימוד בלבד. כשאתם עובדים על הפרויקטים שלכם אתם צריכים להשתמש בספריות וסביבות פיתוח מוכחות, מהירות ובטוחות.
המשתמש באתר צריך להיות מודע לכך שאם וכאשר הוא מפתח קוד בשביל פרויקט הוא חייב לשים לב ולהשתמש בסביבת הפיתוח המתאימה ביותר, הבטוחה ביותר, היעילה ביותר וכמובן שהוא צריך לבדוק את הקוד בהיבטים של יעילות ואבטחה. מי אמר שלהיות מפתח זו עבודה קלה ?
השימוש שלך באתר מהווה ראייה להסכמתך עם הכללים והתקנות שנוסחו בהסכם תנאי השימוש.