איך חוקרים מנגנון חדש¶
נניח שנתקלנו במשפט ״הקריאה לTask.Yield מעבירה את העבודה לthread אחר״. המשפט נשמע סביר, ואפשר למצוא הרצה שבה הוא מתאים לפלט. אבל אנחנו כבר יודעים שדוגמה אחת אינה חוזה. נבנה ניסוי שמפריד בין דחיית המשך העבודה לבין החלפת thread.
הפעם נתחיל מהשערה כתובה, לפני פתיחת המקור. ההשערה אומרת שהקוד אחרי await חייב לרוץ על thread שונה. כדי להפריך אותה מספיק למצוא מסלול חוקי שבו ההמשך נדחה אבל חוזר לאותו thread.
מקבעים את מה שקוראים¶
נשתמש במקור של YieldAwaitable מתוך tag קבוע של runtime. בקובץ אפשר לעקוב אחרי בחירת מסלול התזמון לפי context וscheduler, במקום להניח שכל continuation נשלח לאותו מקום. נקודת ההתחלה היא קוד YieldAwaitable בגרסה 10.0.0.
המקור בtag הוא ראיה לגבי אותו קוד. אם runtime שמריץ את הניסוי הוא גרסת patch אחרת, מציינים זאת ובודקים גם את המקור שלה לפני שמייחסים לה את כל פרטי המימוש. את גרסת סביבת הריצה אפשר להדפיס מתוך התוכנית, ואת commit המקור שומרים בדוח.
יוצרים context שאנחנו מבינים¶
התוכנית הבאה מספקת תור קטן של continuations. הפעולה Post רק מכניסה עבודה לתור; הmain הוא מי שמוציא אותה ומריץ אותה. אין כאן thread חדש ואין message loop של framework שמסתירה את הסדר.
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
sealed class ManualContext : SynchronizationContext
{
readonly ConcurrentQueue<(SendOrPostCallback Callback, object? State)> queue = new();
public int Posted { get; private set; }
public override void Post(SendOrPostCallback callback, object? state)
{
// This experiment posts from the owner thread only.
Posted++;
queue.Enqueue((callback, state));
}
public bool PumpOne()
{
if (!queue.TryDequeue(out var item)) return false;
item.Callback(item.State);
return true;
}
}
static class Program
{
static async Task Probe()
{
Console.WriteLine($"before thread={Environment.CurrentManagedThreadId}");
await Task.Yield();
Console.WriteLine($"after thread={Environment.CurrentManagedThreadId}");
}
static void Main()
{
Console.WriteLine(RuntimeInformation.FrameworkDescription);
var previous = SynchronizationContext.Current;
var context = new ManualContext();
SynchronizationContext.SetSynchronizationContext(context);
try
{
Task operation = Probe();
Console.WriteLine($"before pump completed={operation.IsCompleted} posted={context.Posted}");
if (operation.IsCompleted || context.Posted != 1)
throw new InvalidOperationException("Unexpected scheduling path");
while (!operation.IsCompleted)
if (!context.PumpOne()) throw new InvalidOperationException("No work to pump");
operation.GetAwaiter().GetResult();
}
finally { SynchronizationContext.SetSynchronizationContext(previous); }
}
}
הפרויקט הוא console של .NET 10 עם nullable מופעל. הקוד מיועד לניסוי הזה; הוא אינו dispatcher כללי. בפרט, המונה Posted אינו מסונכרן עבור producers נוספים, והpump אינו ממתין לפעולות עתידיות כמו timer.
הפלט הצפוי מציג אותו thread לפני ואחרי, אבל לפני הpump המשימה עדיין אינה שלמה. השילוב הזה מפריך את ההשערה המקורית. הוא גם מראה למה הסקת מסקנה רק ממספר הthread מאבדת חלק מהסיפור.
מחברים את הפלט למקור¶
נחפש במקור את הדרך שמזהה context מותאם ושולחת אליו continuation. אחר כך נבדוק באילו תנאים נבחר מסלול אחר. לא צריך לקרוא את כל runtime כדי להסביר את הניסוי; צריך לקרוא מספיק כדי לדעת מה התנאי, מי הבעלים של ההמשך ואיפה הוא מתבצע.
נוסיף לניסוי timestamp ומזהה פעולה, ונאסוף trace לפי הכלים מפרק 12. מחפשים התאמה בין Post, תחילת pump וסיום הפעולה. אם אירוע חסר, בודקים קודם אם בכלל ביקשנו את הספק ואת רמת האירוע הנכונים. היעדר שורה מהtrace אינו הוכחה שהקוד לא רץ.
איך אותו תהליך עובד מול WDK sample¶
נבחר callback מסוים מהdriver של פרק 13 ונכתוב השערה באותה צורה. לדוגמה, נבדוק מי רשאי להשלים request אחרי מסירתו לתור ידני. נתחיל מהחוזה המתועד, נמפה את המסלולים בדוגמה ונכניס עצירה בנקודה שבה הבעלות עוברת.
הבדיקה בVM מצרפת גרסת WDK, commit של הדוגמה, symbols ופעולות שחוזרות על המרוץ. כאשר המקור והtrace אינם מסכימים, לא מוחקים את החריגה מהדוח. היא יכולה להעיד על גרסה אחרת, על מסלול שלא קראנו או על השערה שגויה.
ההרגל החשוב הוא לכתוב שלוש שורות נפרדות: מה נצפה, מה אנחנו חושבים שמסביר אותו ואיזה ניסוי יבדיל בין ההסברים. כך גם מנגנון שלא הכרנו אתמול הופך למשהו שאפשר לחקור בלי לנחש.