המודול `concurrent` בפייתון

המודול `concurrent` בפייתון

במאמר זה נסביר את המודול concurrent בפייתון.

תוך כדי הבהרת המושגים ריבוי משימות והרצה מקבילית, נסביר כיצד ליישם עיבוד אסינכרוני באמצעות המודול concurrent עם דוגמאות מעשיות.

YouTube Video

המודול concurrent בפייתון

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

ההבדל בין ריבוי משימות להרצה מקבילית

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

  • הרצה מקבילית היא מנגנון שבו מבצעים פיזית מספר משימות בו-זמנית. באמצעות שימוש במספר ליבות CPU, העיבוד מתקדם בו-זמנית.

שתיהן הן טכניקות להאצת עיבוד, אך ריבוי משימות הוא עניין תכנוני של 'איך מתקדמים', ואילו הרצה מקבילית היא עניין של ביצוע 'איך זה רץ', ולכן מדובר בהבדלים מהותיים.

מהו המודול concurrent?

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

תפקידיי ThreadPoolExecutor ו-ProcessPoolExecutor

המודול concurrent מספק שתי אפשרויות עיקריות בהתאם לאופי המשימה.

  • ThreadPoolExecutor זה מתאים למימושים של ריבוי משימות, במיוחד עבור משימות הכוללות זמן המתנה גבוה ל-I/O, כגון פעולות רשת או קבצים. באמצעות מעבר בין משימות, נוצל זמן ההמתנה בצורה יעילה.

  • ProcessPoolExecutor מימוש זה מיועד לעיבוד מקבילי ומתאים למשימות הדורשות משאבי CPU רבים. הוא משתמש במספר תהליכים כדי לנצל באופן מלא את כל ליבות ה-CPU הזמינות במקביל.

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

יסודות ThreadPoolExecutor (למשימות I/O)

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

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12    for future in futures:
13        print(future.result())
  • בדוגמה זו, מספר משימות I/O שממתינות שנייה מבוצעות במקביל. באמצעות submit, קריאות פונקציה נרשמות כמשימות אסינכרוניות, ודרך result() ניתן להמתין לסיום ולקבל תוצאה, וכך ניתן לממש ריבוי משימות המנצל זמן המתנה בצורה תמציתית.

ריבוי משימות פשוט בעזרת map

אם אין צורך בבקרת ביצוע מורכבת, שימוש ב-map יכול לקצר את קודך.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    results = executor.map(fetch_data, range(5))
11
12    for result in results:
13        print(result)
  • בדוגמה זו, מספר משימות I/O מבוצעות במקביל באמצעות ThreadPoolExecutor.map. מכיוון ש-map מחזיר תוצאות בסדר קלט, תוכל לכתוב קוד המדמה עיבוד סדרתי, ולהשתמש בריבוי משימות מבלי להיות מודע לכך שזה עיבוד אסינכרוני — זה יתרון משמעותי.

יסודות ProcessPoolExecutor (למשימות CPU)

לחישובים כבדים שמנצלים את ה-CPU, יש להשתמש בתהליכים במקום תהליכונים (Threads). כך ניתן לעקוף את מגבלת ה-GIL (נעילת מפענח גלובלית).

 1from concurrent.futures import ProcessPoolExecutor
 2
 3def heavy_calculation(n):
 4    # Simulate a CPU-bound task
 5    total = 0
 6    for i in range(10_000_000):
 7        total += i * n
 8    return total
 9
10if __name__ == "__main__":
11    with ProcessPoolExecutor(max_workers=4) as executor:
12        results = executor.map(heavy_calculation, range(4))
13
14        for result in results:
15            print(result)

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

עיבוד לפי סדר הסיום באמצעות as_completed

as_completed שימושי כאשר רוצים לטפל בתוצאות בסדר שהן מסתיימות.

 1from concurrent.futures import ThreadPoolExecutor, as_completed
 2import time
 3
 4def fetch_data(n):
 5    # Simulate an I/O-bound task
 6    time.sleep(1)
 7    return f"data-{n}"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(fetch_data, i) for i in range(5)]
11
12    for future in as_completed(futures):
13        print(future.result())
  • בדוגמה זו, משימות אסינכרוניות מרובות רצות במקביל, והתוצאות נאספות לפי סדר סיום המשימות. שימוש ב-as_completed מאפשר לטפל בתוצאות באופן מיידי בלי קשר לסדר, כך שזוהי אופציה מתאימה להצגת התקדמות או כאשר נדרש טיפול סדרתי.

טיפול בשגיאות

ב-concurrent, שגיאות נזרקות כאשר קוראים ל-result().

 1from concurrent.futures import ThreadPoolExecutor
 2
 3def risky_task(n):
 4    # Simulate a task that may fail for a specific input
 5    if n == 3:
 6        raise ValueError("Something went wrong")
 7    return n * 2
 8
 9with ThreadPoolExecutor() as executor:
10    futures = [executor.submit(risky_task, i) for i in range(5)]
11
12    for future in futures:
13        try:
14            print(future.result())
15        except Exception as e:
16            print("Error:", e)
  • בדוגמה זו נראה שאפילו אם חלק מהמשימות זורקות שגיאה, המשימות האחרות ממשיכות לבצע וניתן לטפל בשגיאות באופן פרטני כשאוספים את התוצאות. בשימוש ב-Future של concurrent, חשוב שניתן לטפל בהצלחות וכישלונות של עיבוד אסינכרוני בצורה בטוחה.

הנחיות לבחירה בין תהליכונים (Threads) לתהליכים (Processes)

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

בפועל, הקריטריונים הבאים יכולים לעזור בהחלטה.

  • עבור תהליכים עם הרבה המתנות לקלט/פלט, כגון תקשורת או פעולות קבצים, השתמש ב־ThreadPoolExecutor.
  • עבור משימות כבדות שמעמיסות את ה-CPU, יש להעדיף את ProcessPoolExecutor.
  • אם יש הרבה משימות פשוטות, שימוש ב-map מאפשר לכתוב קוד תמציתי יותר.
  • אם חשובה בקרה מדויקת על סדר הביצוע או טיפול בשגיאות, יש לשלב בין submit ל-as_completed.

יתרונות השימוש ב-concurrent

באמצעות המודול concurrent, ניתן לטפל בעיבוד אסינכרוני בצורה בטוחה ואינטואיטיבית.

היתרונות המרכזיים הם:.

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

רק שמירה על עקרונות אלה יכולה לצמצם משמעותית כישלונות במימוש בעזרת concurrent.

סיכום

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

תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video