Net-Base מגזין

24.07.2026

Delphi: TParallel.For עם ממשק התקדמות בטוח לשימוש בריבוי־תהליכים (TThread.Queue) ללא deadlocks

כך תשלבו את Delphi TParallel.For עם ממשק התקדמות בטוח לריבוי‑תהליכים: עדכונים באמצעות TThread.Queue, אגרגציה נקייה, טיפול בביטול ומלכודות Deadlock טיפוסיות בניפוי באגים ובתפעול.

24.07.2026

מהנושא במגזין ליישום בפרויקט

דפי שירות וטכניים רלוונטיים למאמר

מי שב־ Delphi רוצה להפעיל במקביל עבודות בעלות עומס חישובי או I/O, מוצא מהר את עצמו מול ה‑Parallel Programming Library (PPL) ובפרט מול TParallel.For. האפקט לעיתים קרובות מורגש מיד — עד שמנסים באותו נשימה לעדכן ממשק התקדמות (Progress-UI) „סתם כך“. כאן נולדות התקיעות האופייניות: קיפואי UI שנראים אקראיים, ProgressBar שקופצת אחורה, או deadlock מוחלט ברגע שמריצים בדיבאגר צעד־אחר־צעד.

במאמר זה מדובר על TParallel.For threadsichere Progress-UI: תבנית עבודה יציבה שעובדת ב‑VCL וב‑FMX, שמרכזת באופן מסודר עדכוני UI באמצעות TThread.Queue, מתחשבת ב‑Cancel/Abort וממנעת בעקביות את מלכודות ה‑deadlock הנפוצות. המוקד הוא בריאליות תפעולית: התנהגות שניתנת לשחזור, תחומי אחריות ברורים והנחיות דיבאג שמועילות גם כשהתקלה מתרחשת רק „אצל הלקוח“.

מדוע Progress-UI ב‑TParallel.For נכשלת כל כך לעתים קרובות

TParallel.For בדרך כלל רץ על Worker‑Threads מתוך בריכת התהליכים של Delphi. תהליכים אלה אסור שיגעו ישירות ב‑VCL או ב‑FMX Controls, מאחר ש‑UI‑frameworks (Message Loop, Window Handles, Rendering) קשורים ל‑Main Thread. אפילו שורה שנראית תמימה כמו ProgressBar.Position := … מתוך Worker עלולה להוביל להתנהגות בלתי מוגדרת: שגיאות גישה (AV) מזדמנות, חלונות תקועים או עדכונים „מהבהבים“.

התיקון המובן מאליו הוא לעיתים קרובות TThread.Synchronize. זה אמנם מתקן את סוגיית ה־thread‑safety, אך בלולאות מקבילות הוא מהר גורם לבעיה אחרת: צוואר בקבוק סדרתי. כל Worker ממתין ל‑UI‑Thread, וה־UI‑Thread עצמו עסוק ברינדור ובביצוע קריאות Synchronize. תחת עומס זה זה יכול להיראות כמו deadlock — גם אם בפועל מדובר באפקט של starvation/lockstep.

ואז קיימת קטגוריית ה‑deadlock האמיתית: ה‑Main Thread ממתין (למשל באמצעות WaitFor, Task.Wait או בעקיפין דרך קריאות חוסמות) לסיום הפעולה המקבילה, בזמן ש‑Worker‑Threads מנסים לשלוח עבודה אל ה‑Main Thread בעזרת Synchronize או שימוש לא נבון ב‑Queue. התוצאה: ה‑Main Thread ממתין ל‑Workers, וה‑Workers ממתינים ל‑Main Thread.

TThread.Queue vs. TThread.Synchronize: der praktische Unterschied

Unschärfer Debugger-Blick mit Notizen zu Threads und Queue als Kontext für Queue vs Synchronize
בעת דיבאוג רואים במהירות אם ה‑Worker‑Threads ממתינים ל‑Main Thread או רק שמים עדכונים בתור (queued).

שני המנגנונים נועדו להריץ קוד בבטחה ב‑Main Thread. ההבדל הוא בסמנטיקת ההמתנה:

  • TThread.Synchronize: ה‑Worker שקורא ממתין עד שה‑Main Thread יבצע את הקוד. זה „סינכרוני“, מגדיל השהיות ומהווה מרכיב טיפוסי ל‑deadlocks כאשר ה‑Main Thread חסום ברגע הנתון.
  • TThread.Queue: ה-Worker מניח את הקוד רק בתור עבור ה-Main Thread וממשיך לפעול. זה „אסינכרוני“, מפשר בין ה-Threads ובתסריטי Parallel כמעט תמיד הבחירה המחדלית הטובה יותר — בתנאי ששולטים על תדירות העדכונים.

חשוב: Queue אינה כרטיס כניסה חופשי. אם תשלחו עדכון Queue בכל איטרציה של לולאה, אתם מוצפים את תור ה-Main-Thread. אז ה-UI אמנם לא נתקע בגלל Deadlock, אלא בגלל כמות הודעות טהורה. ה-UI ירגיש „איטי“, וסיום העיבוד יתעכב כיוון שעדיין רצות מאות או אלפים של עדכוני UI.

המקרה הקצה שממש כואב: המתנה בתוך ה-UI-Thread

ביישומי ארגונים רואים לעתים קרובות את הזרימה הבאה: מקישים על כפתור ‚התחל‘, ה-UI מושבת, נפתח ProgressDialog, ואז ממתינים באופן סינכרוני עד שהכל מסתיים, ואז מפעילים שוב. תבנית זו היא ליבתן של רבות מהתקלות מסוג Deadlock.

גרסאות טיפוסיות (תלוי בבסיס הקוד):

  • Main Thread מריץ TParallel.For ואחריו קורא ללוגיקת המתנה חוסמת (ישיר או עקיף).
  • ProgressDialog קורא בבונה (Constructor) או ב-OnShow לשגרה שממתינה בפנים.
  • כפתור ‚ביטול‘ מגדיר דגל, אבל ה-Main Thread נשאר בכל זאת בלולאת המתנה.

אם ה-Worker-Threads משתמשים בזמן זה ב-Synchronize, ה-Deadlock כמעט מובטח. גם עם Queue עלול להיווצר תקיעה אם ה-Main Thread חסום ואינו מטפל בהודעות — כי אז גם התור לא יעובד.

ההשלכה התפעולית והבטוחה היא: ה-Main Thread אסור שיחכה בצורה חוסמת על הלולאה המקבילה אם נדרשים עדכוני UI במקביל. במקום זאת, העיבוד צריך להיות מיוצא לחלוטין למשימת רקע, או שיש לארגן „סיום אסינכרוני“ (Callback/פעולת סיום בתור) שמשחרר את ה-UI בסיום.

גישה מסודרת: התקדמות מאוגדת בלבד, עדכוני UI מנוהלים בתדירות

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

תבנית עמידה מורכבת משלוש אחריות מופרדות וברורות:

  • Worker-Threads מבצעים את העבודה עצמה לכל אלמנט/אינדקס. הם מדווחים רק על התקדמות בצורה בטוחה ל-threads (מונה, Queue, Thread-safe Queue).
  • Aggregator (לעתים: ה-Main Thread או טיימר ייעודי ב-UI) מחשב מתוך ההתקדמות מצב UI (מיקום, טקסט, ETA) ומעדכן רכיבי UI. כך נמנעים מעדכונים 1:1 לכל איטרציה.
  • Abschluss (גם הוא ב-Main Thread): להחזיר את ה-UI לפעולה, להציג תוצאה, לסכם שגיאות, לשחרר משאבים.

למה ההפרדה הזו עובדת כל כך טוב: עומס העבודה יכול להיות בתדירות גבוהה (אלפי פריטים), אבל ה-UI צריך רק מעט עדכונים בשנייה. בפועל מספיקים 5–10 עדכונים לשנייה, בעבודות מהירות מאוד אפילו 2–4. כל דבר מעל לכך ברוב המקרים הוא רק רעש ויזואלי ועולה בזמן CPU ב-Message Pump.

ספירה בטוחה לשרשורים: Atomic statt Lock

ל-ProgressBar פשוט לעיתים מספיק מונה אטומי. „אטומי“ פירושו: אינקרמנט וקריאה מתבצעים ללא Race Condition, בדרך כלל באמצעות TInterlocked. כך אתם נמנעים מ-Locks (Critical Sections) ב-Hot Path של הלולאה.

רעיון מינימלי מוכח:

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

יתרון: kein TThread.Queue לכל פריט, אין הצפה של ה-UI. חסרון: אין לכם הודעות מפורטות לכל פריט (למשל שם קובץ). ניתן להוסיף הודעת סטטוס שנייה וממוּתבת (ראו הסעיף הבא).

הודעות סטטוס ללא ספאם: „הסטטוס האחרון מנצח“

אם אתם רוצים בנוסף להציג טקסט קצר (הפריט הנוכחי, שלב, הודעת שגיאה), נדרש דגם שגם הוא לא יציף את ה-UI בכל צעד של ה-Worker. בפועל הדגם „הסטטוס האחרון מנצח“ עובד מאוד טוב:

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

כך ה-UI נשאר תגובתי, ועם זאת אתם רואים שמשהו קורה. מה שחשוב כאן פחות המחרוזת עצמה ויותר מחזור החיים: אין לגרור הפניות לאובייקטים קצרי-מועד משרשורי ה-Worker ל-UI-Thread. אם אתם מעבירים אובייקטים, הבהירו במפורש את ה-Ownership.

TParallel.For threadsichere Progress-UI mit TThread.Queue: ein robustes Muster

יש תרחישים שבהם טיימר של ה-UI לבדו לא מספיק: למשל כאשר רוצים בסיום להבטיח בדיוק עדכון „Fertig“ אחד, או כאשר עדכון ה-UI הוא צעד מורכב יותר (למשל רישום בחלון לוג, אך ממוּתב). אז מתאים להשתמש ב-TThread.Queue — אבל לא בכל איטרציה, אלא באופן ממוקד.

גישה פרקטית היא Queue רק עבור אירועים בתדירות נמוכה:

  • Start-Event (הכנת ה-UI, נעילת כפתורים)
  • Periodische Progress-Events (מקסימום כל X מילישניות)
  • Fehler-Events (אופציונלי: נאספים)
  • Done-Event (איפוס ה-UI, הצגת תוצאה)

את המחזוריות אתם לא משיגים דרך ה-UI-Thread, אלא כבר בהקשר ה-Worker: אתם מאפשרים ל-Worker רק אז ein UI-Update queuen, אם מאז העדכון האחרון עבר זמן מספיק. לשם כך מתאימה מקור זמן מונוטוני (למשל TickCount) בתוספת ערך אטומי של „last update“.

חשוב: עדכון ה-UI עצמו חייב להיות „מהיר“. חישובים כבדים, קלט/פלט קבצים או גישות למסד נתונים אינם חלק מה-UI-Callback שנשלח לתור. ה-Callback צריך רק לקרוא מצבים ולהגדיר Controls.

Cancel-Handling: Abbrechen ohne Hänger

ביישומים אמיתיים ביטול איננו אופציונלי. העיקרון הוא: Cancel איננו „Kill“, אלא סיום שיתופי. ה-Worker חייבים לבדוק באופן סדיר אם אות ביטול הוגדר, ואז לצאת בצורה נקייה. ב-Delphi יש מספר דרכים לכך (תלוי בקונסטרוקציית ה-PPL): דגל Volatile משלו, Boolean אטומי, או מושג Cancellation דרך Tasks (תלוי בגרסת Delphi ובמבנה).

להפעלת המערכת חשובים שני כללים:

  • ביטול חייב להיות גלוי במהירות: בדקו את דגל הביטול במקומות המתאימים, לא רק בסוף איטרציה, כאשר האיטרציה עלולה להימשך שניות.
  • ביטול חייב לנקות אחריו: לא ישארו Handles פתוחים, קבצים זמניים, טרנזקציות או Locks. כלומר: בכל איטרציה של Worker חובה להשתמש בבלוקים מסוג try/finally אם מעורבים משאבים.

מבחינת ה‑UI, פעולת הביטול צריכה רק להגדיר אות ולהעביר את ה‑UI למצב «Stopping…». הסיום האמיתי והחזרת הממשק לפעולה יתבצעו באירוע ‚Done‘, ולא מיד בעת הלחיצה.

הימנעות מ‑Deadlocks: המלכודות השכיחות בפרקטיקה

Diagramm eines zyklischen Wartens zwischen Threads als Visualisierung eines Deadlocks
Deadlocks נוצרים לעיתים קרובות ממחזורי המתנה: ה‑UI‑Thread חסום, Worker ממתינים לגישה ל‑UI.

מלכודת 1: WaitFor/Task.Wait ב‑Main Thread

כאשר ה‑Main Thread חסום, הוא לא יכול להריץ Queue‑Callbacks ולא לעבד Messages. זה יתאפיין כ‑Deadlock גם אם ה‑Worker ממשיכים לרוץ כהלכה. פתרון: אין לבצע Waits חוסמים ב‑UI‑Thread. במקום זאת בצעו את פעולת הסיום דרך TThread.Queue או באמצעות מנגנון מבוסס אירועים (למשל טיימר שבודק אם ה“מוכן“).

מלכודת 2: Synchronize בתוך Lock

קלאסיקה: ה‑Worker מחזיק Critical Section, קורא ל‑Synchronize, וב‑UI‑Callback נדרשת שוב אותה Critical Section (ישירות או בעקיפין). תוצאה: המתנה מעגלית. הכלל פשוט: אין להעביר ל‑UI (Synchronize/Queue) מתוך Lock מוחזק. אם Lock נחוץ, קחו את כל הנתונים למשתנים מקומיים, שחררו את ה‑Lock, ואז בצעו את ה‑queue.

מלכודת 3: UI‑Callback מפעיל כניסה חוזרת (reentrancy)

לפעמים עדכון ה‑UI עצמו אינו „חף מפשע“: שינוי Properties יכול להצית Events (OnChange, OnResize) שמפעילים לוגיקה הנגשת למצב ה‑Worker. זה אינו Deadlock במובן הצר, אך מוביל לתלישות שקשה להסביר ול־Race Conditions. פתרון: העבירו עדכוני UI לנתיבים „שקטים“ (השביתו Events זמנית) או השתמשו ב‑Reentrancy‑Guards (למשל Guard אטומי לשלב העדכון).

מלכודת 4: יותר מדי עדכונים שהוכנסו ל‑queue

גם בלי Waits ה‑UI עלול „לעמוד“ אם מייצרים עשרות אלפי queued Callbacks. סימפטומים: ה‑ProgressBar ממשיכה לרוץ זמן רב, החלון מגיב באיטיות, שימוש ה‑CPU ב‑Main Thread גבוה. פתרון: להגביל (חלון זמן), לאחד (Counter), או להשתמש במבנה Producer/Consumer אמיתי שבו רק עדכון UI אחד יכול להיות pending (Coalescing).

כשזה מסתבך: איסוף תוצאות, ריכוז שגיאות, הבטחת סדרים

TParallel.For אידיאלי כאשר האיטרציות בלתי תלויות. בתוכנות עסקיות האיטרציות לעיתים קרובות רק „במקום־מה“ בלתי תלויות: קוראים קבצים, קוראים ל‑REST‑APIs, כותבים שורות בבסיס הנתונים. אז יש לתכנן באופן מסודר שלוש נקודות נוספות:

  • איסוף תוצאות Thread‑בטוח: או שכל Thread ישמור Buffer מקומי (לאחד בסוף) או להשתמש ב‑Queue/Collection בטוחה לריבוי־תהליכים. הימנעו מ‑Locks ב‑Hot Path.
  • טיפול בשגיאות: יש לאסוף Exceptions מתוך Worker-Threads. בפועל מוכח ששיטה יעילה היא: לזכור את ה-Exception הראשון ולהפעיל Cancel, או לאסוף את כל ה-Exceptions ולהציגם מסוכמים בסוף.
  • סדר: אם הפלט צריך סדר יציב (למשל לוגים לפי אינדקס), עיבוד מקבילי עם מיון מאוחר הוא לעתים קרובות פשוט יותר מאשר „thread-sicheres geordnetes Einfügen“.
  • עבור ה-UI זה אומר: אל תציגו כל הודעת שגיאה בנפרד באופן מיידי. הדבר מוביל לגיהנום דיאלוגים מודאליים. אספו שגיאות (למשל רשימת מחרוזות), והציגו בסוף סיכום או לוג שניתן לייצא.

    ניפוי שגיאות: איך לגרום ל-deadlock להיות נראה באמת

    Deadlocks בקוד מקבילי מתסכלים, כי הם יכולים להיראות בדיבאגר שונה מאשר ב-Release. עם זאת, יש כמה מנופים פרקטיים מאוד:

    השתמשו בחלון Threads וב-Call Stacks

    אם ה-UI תקוע, בדקו את כל ה-Threads: איפה עומד ה-Main Thread? האם הוא מחכה? האם הוא בלולאת הודעות (Message-Loop)? איפה עומדים ה-Worker-Threads? אם ה-Worker תקועים ב-Synchronize, הסיבה כמעט תמיד היא „Main Thread חסום“ או „Main Thread צריך Lock“.

    סימון נקודות Queue / Synchronize

    בצעו לוגינג ממוקד בנקודות העברה (לפני ה-Queue, ב-Queue-Callback, בסוף האיטרציה). בתפעול זה לעתים קרובות שימושי יותר מ-Breakpoints, כי ה-Timing קריטי. שימו לב שהלוגינג עצמו thread-safe ולא חוסם (למשל, אין פלט לוג ל-UI ישירות מתוך Worker-Threads).

    מדידת זמן העדכון האחרון

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

    מתי כדאי באמת להשתמש ב-TParallel.For עם Progress-UI?

    מקביליות איננה מטרה בפני עצמה. היא משתלמת במיוחד כאשר:

    • האיטרציות גדולות מספיק (מילישניות עד שניות), כך שה-Threadpool-Overhead נשמט,
    • העבודה היא כבדת CPU (Parsing, Kompression, Hashing) או שיש לה I/O הניתנת לפרללה בקלות (קבצים מרובים, מספר HTTP-Requests עם הגבלות),
    • יש לכם אסטרטגיית Cancel ושגיאות ברורה,
    • דרישות ה-UI מסתפקות בהתקדמות מצטברת (aggregated Progress).

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

    רשימת בדיקה מעשית: כך ה-UI נשאר יציב

    • ה-Main Thread לא נחסם: אין Waits, אין לולאות ארוכות ללא Message Pump.
    • ל-Worker אין גישה ל-Controls: אין גישות VCL/FMX מחוץ ל-UI-Thread.
    • עדכוני ה-UI מוגבלים: Counter/Timer או coalesced Queue-Updates במקום עדכון לכל איטרציה.
    • אין קריאות Synchronize מתוך Locks.
    • ה-Cancel הוא שיתופי, נבדק בתדירות ומנקה משאבים כראוי.
    • Exceptions נאספים ומטופלים בסדר בסוף.

    מסקנה: לנתק עם Queue, לייצב באמצעות Aggregation

    ממשק התקדמות יציב ב-TParallel.For אינו נוצר על ידי „לשים Synchronize איפשהו“, אלא על ידי עיקרון ארכיטקטוני ברור: ה-workers פועלים באופן עצמאי, ה־Main Thread נשאר פנוי ומעבד רק מספר מועט של עדכוני UI מהירים. TThread.Queue הוא הכלי המתאים אם תשתמשו בו במכוון ובאופן מבוקר. להתנהגות „אטית“ או לא יציבה יש כמעט תמיד שתי סיבות: ה־Main Thread ממתין במקום חוסם – או שהוא טובע במספר רב מדי של עדכוני תור.

    אם תגדירו את התבנית הזו כראוי (Counter/Coalescing, Cancel-Flag, Abschluss-Callback), כדאי ליישם אותה על פני אזורים רבים ביישום Delphi קיים: Import/Export, בדיקות נתונים, עבודות קבצים ו-API – הכל יהפוך לרמת תגובתיות גבוהה יותר, מבלי שתצברו deadlocks חדשים בכל עדכון התקדמות.

    אם אתם זקוקים לתמיכה בייצוב קוד מקבילי, בניתוח (debugging) של תקיעות UI או במודרניזציה מסודרת של יישומי Delphi קיימים: צרו קשר.

    לנושא זה גם חשובות Delphi Parallel Programming Library ו-Tthread.queue Vs Synchronize. המאמר מסדר היבטים אלה באופן מובן ומדגים מה חשוב בעבודה היומיומית.

    לדון בפרויקט או במיזם מודרניזציה עם Net-Base.

    השלב הבא

    Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.

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

    • המצב הקיים, תמונת היעד והסיכונים הטכניים מוערכים יחד.
    • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
    • מבעוד מועד אתם רואים איזה נתיב בר-קיימא מבחינה כלכלית ותפעולית.

    שתף פוסט

    לשתף את הפוסט הזה ישירות

    LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. für Instagram bereiten wir Link und Kurztext direkt vor.

    דוא״ל

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