練習(xí):自己動(dòng)手實(shí)現(xiàn)一個(gè)輕量級(jí)的信號(hào)量(二)
話說(shuō)看了Angel Lucifer兄的留言之后,發(fā)現(xiàn)果然Microsoft在June CTP中實(shí)現(xiàn)了SemaphoreSlim,其中不但考慮了與舊的同步對(duì)象在接口上的一致性,還加入了Cancellation的檢查。唉,怪我沒(méi)有跟上形勢(shì)!那么這篇就成班門弄斧了。不過(guò),還應(yīng)該堅(jiān)持把它寫完,善始善終,就當(dāng)為大家整理思路。取笑罷了:-)
上一次說(shuō)到用戶態(tài)與內(nèi)核態(tài)切換帶來(lái)的開銷平均水平是600個(gè)時(shí)鐘周期!因此,如果在大部分時(shí)間內(nèi)。各個(gè)線程對(duì)同步對(duì)象操作時(shí)間非常短暫的情況下,可以先自旋一段時(shí)間,避免直接進(jìn)入內(nèi)核模式,在自旋無(wú)果的情況下再進(jìn)入真正的等待。(這里插一句:可能我說(shuō)的不清楚,造成了大家的誤解,實(shí)際上獲得一個(gè)CriticalSection的操作在用戶態(tài)就可以完成了,但是,一旦鎖已經(jīng)被其他線程持有而需要進(jìn)入等待時(shí),一定會(huì)切換到內(nèi)核模式。這就是為什么說(shuō)使用自旋可以極大限度的避免每次都切換到內(nèi)核模式的原因)。Intel的《多核程序設(shè)計(jì)》一書中就提到,在多核處理器平臺(tái),可以使用
初始化CriticalSection對(duì)象,使其在進(jìn)入真正等待之前進(jìn)行短暫的自旋而不是直接進(jìn)入等待狀態(tài),也是這個(gè)意思。
好的,首先看看我們?cè)谑裁吹胤叫枰M(jìn)行自旋等待。很顯然就是在Wait剛剛開始,需要獲得鎖的時(shí)候。為了方便,將上一篇中的源代碼摘抄過(guò)來(lái)。
2 {
3 // 參數(shù)檢查工作是必須的
4 if (millisecondsTimeout < -1)
5 throw new ArgumentOutOfRangeException("millisecondsTimeout");
6 if (waitResNumber < 1)
7 throw new ArgumentOutOfRangeException("waitResNumber");
8 // 我們?cè)诤竺嬉獌纱翁幚沓瑫r(shí),因此我們需要一個(gè)計(jì)時(shí)器
9 System.Diagnostics.StopWatch watch = null;
10 int timeoutNum = millisecondsTimeout;
11 if (millisecondsTimeout != -1)
12 {
13 watch = System.Diagnostics.Stopwatch.StartNew();
14 }
15 // 現(xiàn)在我們來(lái)做第(1)步
16 if (!System.Threading.Monitor.TryEnter(this._globalLock))
17 {
18 if (millisecondsTimeout == 0)
19 return false;
20 /////////////////////////////////////////////////
21 // 就是在這里,我們需要先自旋一下,然后再進(jìn)入真正的等待 //
22 /////////////////////////////////////////////////
23 if (!System.Threading.Monitor.TryEnter(this._globalLock, millisecondsTimeout))
24 {
25 return false;
26 }
27 }
28 // 現(xiàn)在我們已經(jīng)獲得鎖了,我們要增加阻塞的線程數(shù)目,以便將來(lái)有人能夠喚醒我們
29 ++this._waitingThreadCount;
30 try
31 {
32 // 如果當(dāng)前的資源數(shù)目不夠用的,我們就得等等了
33 while (this._currentResources - waitResNumber < 0)
34 {
35 if (millisecondsTimeout != -1)
36 {
37 // 看看是不是超時(shí)了
38 timeoutNum = UpdateTimeout(watch, millisecondsTimeout);
39 if (timeoutNum <= 0)
40 {
41 return false;
42 }
43 }
44 // 如果沒(méi)有超時(shí)我們就再等一下
45 if (!System.Threading.Monitor.Wait(this._globalLock, timeoutNum))
46 {
47 return false;
48 }
49 // 好的我們被喚醒了,趕快回去檢查檢查有沒(méi)有足夠的資源了
50 }
51 // 很好,我們現(xiàn)在有足夠的資源了,
52 // 并且沒(méi)有什么人能夠再更改資源數(shù)目了,因?yàn)殒i在我們手里!
53 this._currentResources -= waitResNumber;
54 }
55 finally
56 {
57 // 很好,我們安全的退出了,減少阻塞的線程數(shù)目并且釋放鎖
58 --this._waitingThreadCount;
59 System.Threading.Monitor.Exit(this._globalLock);
60 }
61 return true;
62 }
63
64
直接在上面添加一個(gè)Thread.SpinWait并不很好,因?yàn)槿绻淮蜸pin的時(shí)間較長(zhǎng),就無(wú)謂的浪費(fèi)了時(shí)間,而過(guò)短又不能避免進(jìn)入真正的等待狀態(tài),所以最好是分幾次來(lái)Spin,這樣,在每一次的間歇我們還是可以檢查是否我們已經(jīng)獲得了鎖。自然,要多加上一個(gè)循環(huán):
2 {
3 // 參數(shù)檢查工作是必須的
4 if (millisecondsTimeout < -1)
5 throw new ArgumentOutOfRangeException("millisecondsTimeout");
6 if (waitResNumber < 1)
7 throw new ArgumentOutOfRangeException("waitResNumber");
8 // 我們?cè)诤竺嬉獌纱翁幚沓瑫r(shí),因此我們需要一個(gè)計(jì)時(shí)器
9 System.Diagnostics.Stopwatch watch = null;
10 int timeoutNum = millisecondsTimeout;
11 if (millisecondsTimeout != -1)
12 {
13 watch = System.Diagnostics.Stopwatch.StartNew();
14 }
15 // 現(xiàn)在我們來(lái)做第(1)步
16 // 這個(gè)值實(shí)際上很有講究,可惜這個(gè)地方我沒(méi)有做很多的測(cè)試
17 int spinThreshold = 20;
18 while (true)
19 {
20 if (!System.Threading.Monitor.TryEnter(this._globalLock))
21 {
22 // 如果不允許超時(shí)設(shè)置則一次獲得失敗就直接退出
23 if (millisecondsTimeout == 0)
24 return false;
25 // 如果不是無(wú)限等待每次循環(huán)都更新時(shí)間看看有沒(méi)有超時(shí)
26 else if (millisecondsTimeout != -1)
27 {
28 timeoutNum = UpdateTimeout(watch, millisecondsTimeout);
29 if (timeoutNum <= 0)
30 return false;
31 }
32 if (spinThreshold <= 0)
33 {
34 // 如果自選完畢了就進(jìn)入真正的等待
35 if (!System.Threading.Monitor.TryEnter(this._globalLock, millisecondsTimeout))
36 {
37 return false;
38 }
39 else
40 {
41 // 我們最終還是獲得了鎖
42 break;
43 }
44 }
45 else
46 {
47 // 退避自旋
48 System.Threading.Thread.SpinWait(21 - spinThreshold);
49 --spinThreshold;
50 }
51 }
52 else
53 {
54 // 很幸運(yùn),第一次就獲得了鎖!
55 break;
56 }
57 }
58 // 現(xiàn)在我們已經(jīng)獲得鎖了,我們要增加阻塞的線程數(shù)目,以便將來(lái)有人能夠喚醒我們
59 ++this._waitingThreadCount;
60 try
61 {
62 // 如果當(dāng)前的資源數(shù)目不夠用的,我們就得等等了
63 while (this._currentResources - waitResNumber < 0)
64 {
65 if (millisecondsTimeout != -1)
66 {
67 // 看看是不是超時(shí)了
68 timeoutNum = UpdateTimeout(watch, millisecondsTimeout);
69 if (timeoutNum <= 0)
70 {
71 return false;
72 }
73 }
74 // 如果沒(méi)有超時(shí)我們就再等一下
75 if (!System.Threading.Monitor.Wait(this._globalLock, timeoutNum))
76 {
77 return false;
78 }
79 // 好的我們被喚醒了,趕快回去檢查檢查有沒(méi)有足夠的資源了
80 }
81 // 很好,我們現(xiàn)在有足夠的資源了,
82 // 并且沒(méi)有什么人能夠再更改資源數(shù)目了,因?yàn)殒i在我們手里!
83 this._currentResources -= waitResNumber;
84 }
85 finally
86 {
87 // 很好,我們安全的退出了,減少阻塞的線程數(shù)目并且釋放鎖
88 --this._waitingThreadCount;
89 System.Threading.Monitor.Exit(this._globalLock);
90 }
91 return true;
92 }
這樣我們就針對(duì)鎖的獲得加入了自旋優(yōu)化,充分避免直接進(jìn)入等待鎖的狀態(tài)。
剩下的就是以秋風(fēng)掃落葉之勢(shì)編寫余下的代碼了!整體代碼如下:
上面有一處小小的改動(dòng)。就是_waitingThreadCount取消了volatile修飾,因?yàn)槲覀儗?duì)于這個(gè)變量的更新以及獲得均在lock中,已經(jīng)包含了適當(dāng)?shù)腷arrier,因此沒(méi)有必要將其聲明為volatile了。相反的,_currentResourceCount 就需要聲明為 volatile 以保證其有acquire語(yǔ)義。
回頭再看一下微軟實(shí)現(xiàn)的SemaphoreSlim,確實(shí)考慮的很周到,不但考慮了Cancellation(但是很奇怪,一個(gè)Semaphore一旦取消之后就貌似不可能重新恢復(fù)了,只有Dispose之后再用另一個(gè)?。┒铱紤]了與原有的同步對(duì)象的兼容性,方便代碼移植。繼承自WaitHandle,在需要使用內(nèi)核對(duì)象時(shí)Lazy創(chuàng)建并使用內(nèi)核對(duì)象,而在不使用WaitHandle的情況下,使用Monitor對(duì)Semaphore進(jìn)行模擬。在這一點(diǎn)上,確實(shí)應(yīng)當(dāng)學(xué)習(xí)Microsoft,多從用戶(對(duì)于Library,程序員就是用戶?。┑慕嵌瓤紤]。畢竟得用戶者得天下啊。(在東西不是很貴的前提下,哈哈:-D)
話說(shuō)看了Angel Lucifer兄的留言之后,發(fā)現(xiàn)果然Microsoft在June CTP中實(shí)現(xiàn)了SemaphoreSlim,其中不但考慮了與舊的同步對(duì)象在接口上的一致性,還加入了Cancellation的檢查。唉,怪我沒(méi)有跟上形勢(shì)!那么這篇就成班門弄斧了。不過(guò),還應(yīng)該堅(jiān)持把它寫完,善始善終,就當(dāng)為大家整理思路。取笑罷了:-)





}
}
浙公網(wǎng)安備 33010602011771號(hào)