יצא לי לאחרונה לטפל בבעיות ביצועים בספריה שלי. מבלי להכנס ליותר מדי פרטים, הספריה היא (בין השאר) ספריית Server המבוססת על WebSockets, וככזאת היא מטפלת בClientים מחוברים.
ברגע שהClient מצליח להתחבר לServer, הServer צריך להקצות לו מזהה ייחודי (הנקרא גם SessionId) - זהו מספר רנדומלי שלם בין 0 ל$ 2^{50} $ (לא כולל הקצוות. יכול להיות שהקצה הוא לא בדיוק $ 2^{50} $ אלא משהו שאני לא זוכר כרגע, אבל זה לא מהותי להמשך הפוסט).
אילוסטרציה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
publicclassServer
{
privatereadonly Random mRandom = new Random();
privatereadonly ClientProxyContainer mClientContainer = new ClientProxyContainer();
privateconstlong MAX_RANDOM_SIZE = 2 >> 50;
publicvoidOnNewClient(IConnection connection)
{
ClientProxy clientProxy = new ClientProxy(connection);
(פישטתי קצת את הסיפור, כעקרון צריך שהClientContainer יבחר את הSessionId, באופן שיצא חד-חד-ערכי. בנוסף, אין Overload לRandom שמקבל long, אבל אנחנו נתעלם מהבעיות האלה כיוון שהן לא מהותיות בהקשר של הבעיה)
הספריה כמובן מקבלת את הClientים בצורה א-סינכרונית, מה שאומר שכשClient מתחבר, מתעורר Thread מהThreadPool (באמצעות מנגנון הIOCP) ומתחיל לטפל בו. בדרך כלל הסיפור עבד בסדר גמור, אבל במקרי קיצון (מספר גדול מאוד של קליינטים או הרבה חיבורים/ניתוקים בו זמנית), יצא מצב שכל הקליינטים שהתחברו קיבלו את אותו SessionId שערכו היה 0. (למי שקורא את מה שכתוב בסוגריים - במימוש הראשון לא וידאתי שאכן הSessionId חד-חד-ערכי. במידה והייתי מוודא זאת, במקום לקבל SessionId=0 בכל הקליינטים, השרת היה נתקע בלולאות אינסופיות)
דיבגתי את הקוד וראיתי שRandom מחזיר במקרה קיצון זה 0 תמיד. חיפשתי באינטרנט על כך, ומצאתי פוסט בבלוג של MSDN מ2009. בפוסט זה מציינים שRandom אינו Thread-safe והוא ממומש בצורה שגורמת לכך שאם הוא נקרא ממספר Threadים במקביל, הוא יחזיר 0. בפוסט זה מציעים שני פתרונות לבעיה זו. הפתרון הראשון שהוא מאוד פשוט הוא להשתמש במנגנון נעילה:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
publicclassServer
{
privatereadonly Random mRandom = new Random();
privatereadonly ClientProxyContainer mClientContainer = new ClientProxyContainer(
privatereadonlyobject mLock = newobject();
privateconstlong MAX_RANDOM_SIZE = 2 >> 50;
publicvoidOnNewClient(IConnection connection)
{
ClientProxy clientProxy = new ClientProxy(connection);
שימו לב שאנחנו נועלים רק בפעם הראשונה בכל Thread כשנוצר הRandom הפנימי. הבעיה בפתרונות אלה שמוצעים בפוסט המדובר היא שמדובר במשתנה סטטי. עדיף לעבוד עם משתנה שהוא גם פר Instance.
למזלנו קיים בFramework טיפוס בשם ThreadLocal המאפשר את הסיפור הזה. (למי שלא מכיר, זהו טיפוס שעוטף את LocalThreadStorage בצורה נוחה)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
publicclassThreadSafeRandom
{
privatereadonly ThreadLocal<Random> mRandom;
privatereadonlyobject mLock = newobject();
privatereadonly Random mSeedGenerator = new Random();
publicThreadSafeRandom()
{
mRandom = new ThreadLocal<Random>(() => GetRandom());
}
private Random GetRandom()
{
int seed;
lock (mLock)
{
seed = mSeedGenerator.Next();
}
Random random = new Random(seed);
return random;
}
public Random Random
{
get
{
return mRandom.Value;
}
}
}
publicclassServer
{
privatereadonly ClientProxyContainer mClientContainer = new ClientProxyContainer();
privatereadonly ThreadSafeRandom mThreadSafeRandom = new ThreadSafeRandom();
privateconstlong MAX_RANDOM_SIZE = 2 >> 50;
publicvoidOnNewClient(IConnection connection)
{
ClientProxy clientProxy = new ClientProxy(connection);