Don't Rely on Environment.ProcessorCount
אחת התורות הנסתרות בפיתוח מקבילי היא השאלה "בכמה ת'רדים צריך להשתמש כדי להגיע לניצול מירבי של החומרה העומדת לרשותנו?". יש שיגידו שמספר הת'רדים צריך להיות כמספר המעבדים, או כמספר המעבדים +1, או אולי בכלל פי 2 ממספר המעבדים. הסיבה שיש כל כך הרבה תשובות לשאלה, היא פשוט בגלל הסיבה שעבור כל תרחיש מסויים, תתאים תשובה אחרת (לכל אפליקציה יש אופי שונה, למשל האם היא מוגבלת על ידי ה-CPU או ה-IO?). אבל בכל אופן, תמיד הנוסחאות האלה מתבססות באיזשהיא צורה על מספר המעבדים הזמינים לנו (סך הכל, אנחנו רוצים להיות כמה שיותר Scalable כשזה נוגע להוספת מעבדים).
בדרך כלל כשהאפליקציה מריצה את רוטינת האיתחול שלה ומתכוננת ליצור את רשימת ה-Worker Threads שלה, היא תבדוק כמה מעבדים קיימים על המחשב כדי להגיע לאותו "מספר קסם" ממקודם. כדי לדעת מהמספר המעבדים הקיימים, בדרך כלל פונים ל-Environment.ProcessorCount. מה שהפרופרטי הזה בסך הכל עושה, זה לפנות ל-Environment Variable הנקרא "NUMBER_OF_PROCESSORS", ולהחזיר את הערך שלו בתור מספר.
הבעיה היא שאותו ערך לא משקף כלל את מספר המעבדים שבאמת זמינים ל-Process שלנו. בתסריט מסויים, המשתמש שהפעיל את האפליקציה החליט להעניק לה Affinity כלשהו, שבפועל יגרום לאפליקציה להשתמש רק בחלק זעום ממספר המעבדים הקיימים במחשב (נניח שעל המחשב יש 64 מעבדים, אבל לפרוסס נקבע לרוץ רק על אחד מהם). מה שיקרה בסיטואציה הזאת, היא שהאפליקציה אמנם תיצור ת'רדים כמספר המעבדים הקיימים (64), אבל לא כמספר המעבדים הזמינים לה (1). כך שכל אותם ת'רדים למעשה יחלקו את אותו מספר מצומצם של מעבדים, מה שבאופן בלתי נמנע יוביל לכמות לא מבוטלת של Context Switch'ים שפשוט יהרגו את ביצועי האפליקציה.
התסריט של קביעת Affinity הוא רחוק מלהיות מופרך מאחר ובסיטואציה בה האפליקציה שלנו מנצלת כל הזמן את כל המעבדים העומדים לרשותה, ואנחנו מעונינים להריץ על אותו המחשב, במקביל אליה, אפליקציה אינטנסיבית אחרת, נהיה חייבים לקבוע Affinity מתאים עבור 2 האפליקציות כדי שלא "יפריעו" אחת לשני. אך במידה והאפליקציות מתעלמות מה-Affinity שנקבע להן, אנחנו נמצא את עצמנו שורפים Cycle'ים בלי סיבה.
בדוט-נט אפשר לקבל את ערך ה-Affinity דרך הפרופרטי Process.ProcessorAffinity. מדובר ב-Bitmask בו כל ביט דלוק מייצג מעבד עליו הפרוסס שלנו יכול לרוץ (במידה וכולם כבויים, ה-Scheduler יחליט בעצמו באילו מעבדים להשתמש, כך שלמעשה כל המעבדים זמינים). כברירת מחדל עבור כל מעבד שזמין למערכת ההפעלה, הביט התואם יהיה דלוק. מכאן שמערכות הפעלה של 32 ביט יכולות לפנות ל-32 מעבדים, ואילו מערכות הפעלה של 64 ביט יכולות לפנות ל-64 מעבדים). עם זאת, בגרסאות האחרונות של Windows קיימת תמיכה גם במעל ל-64 מעבדים. כדי לפנות לכל המעבדים הללו משתמשים ב-Groups, כאשר כל Group יכול לפנות לעד 64 מעבדים השייכים לו. כך שאותו Bitmask שמייצג את ה-Affinity, למעשה מייצג את ה-Affinity בתוך ה-Group המיוחס (כברירת מחדל, פרוסס משתמש במעבדים מתוך Group אחד בלבד).
אז בכל אופן, כדי לעבוד בצורה מתחשבת ולתמוך ב-Affinity שנקבע לפרוסס שלנו, אנחנו למעשה צריכים לספור את מספר הביטים הדולקים באותו Bitmask .
לצורך ההדגמה, התוכנית הזאת בודקת כמה מעבדים זמינים לפרוסס, ומדפיסה על אילו אינדקסי מעבדים היא יכולה לרוץ.
static void PrintAffinitizedProcessors()
{
// gets the number of affinitized proccesors in the
// current processor group (up to 64 logical processors)
Process currentProcess = Process.GetCurrentProcess();
long affinityMask = (long)currentProcess.ProcessorAffinity;
if (affinityMask == 0)
affinityMask = (long)Math.Pow(Environment.ProcessorCount, 2) - 1;
const int BITS_IN_BYTE = 8;
int numberOfBits = IntPtr.Size * BITS_IN_BYTE;
int counter = 0;
for (int i = 0; i < numberOfBits; i++)
{
if ((affinityMask >> i & 1) == 1)
{
Console.WriteLine(i);
counter++;
}
}
Console.WriteLine("Total: " + counter);
}