轉帖|其它|編輯:郝浩|2011-05-26 14:55:29.000|閱讀 1400 次
概述:最近發表過一些對c#多線程數據讀寫安全線的文章,有網友說都是代碼不好理解,我在這里就給出我的一些解釋,希望大家多多指較.這里我重復一下多線程數據讀寫安全的觀點:多線程下的數據安全應該指的是在使用數據的生存期內它是不變的,使用數據的生存期可以是一個過程或函數,當然這里的指的數據不包含過程或函數中的局部變量,因為局部變量它本身就是線程安全的數據.
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
最近發表過一些對c#多線程數據讀寫安全線的文章,有網友說都是代碼不好理解,我在這里就給出我的一些解釋,希望大家多多指較.這里我重復一下多線程數據讀寫安全的觀點:多線程下的數據安全應該指的是在使用數據的生存期內它是不變的,使用數據的生存期可以是一個過程或函數,當然這里的指的數據不包含過程或函數中的局部變量,因為局部變量它本身就是線程安全的數據.
目標:確保數據在使用的生命期內是不變的.
解決思路:對于數額的使用不外乎就是讀和寫,而讀操作對數據是不會產生變化的,僅有寫操作才對數據產生變化,試想一下兩個線程同時對同一數據進行讀和寫操作,那么讀出的數據很可能在使用過程中被寫操作改變了,那么依據原先讀出的數據進行的邏輯過程差之豪厘失之千理了.大家且看下面的一個例子,該例子的類有這么一個邏輯,它有一個狀態變量Disposed,當它為False時該類的所有方法可用,當它為True時該類的所有方法不可用,如果使用就出現錯誤.而該類有一個方法Function1做一些操作,有一個方法Close關閉該類同時設置該類的狀態變量為False.
1 public class DIO
2 {
3
4 private bool Disposed = false;
5
6 public void Function1()
7 {
8 if (!Disposed)
9 {
10 //如果該類的狀態參數有效則進行一系列操作
11 //否則以下的語句就會出錯
12 ......
13 ......
14 ......
15 .....
16 .....
17 }
18 }
19
20 public void Closed()
21 {
22 if (!Disposed)
23 {
24 //如果該類的狀態參數有效則進行一系列清理操作
25 //否則以下的語句就會出錯
26
27 //并設置狀態參數為true
28 .......
29 ......
30 .......
31 .......
32 ........
33 .........
34 Disposed = true;
35
36 }
37 }
38 }
假設有兩個線程正分別執行Function1和Close方法,當Function1方法執行到第8句時發現Disposed參數為False所以它繼續下面的操作,而此時Close方法已執行完所有清理工作并對Disposed做了設置,那么此時Function1里執行的方方法就肯定出錯了(這也解答了有網友說的對寫同步讀就沒有必要的問題了),因為此時該類已進行過一系列的清理工作了.這也就是說要實現該類的邏輯并且線程安全,就必需對Disposed變量進行同步,那么對該類進行如下更改:
1 public class DIO
2 {
3
4 private bool Disposed = false;
5
6 private object LockObject = new object();
7
8 public void Function1()
9 {
10 lock(LockObject)
11 {
12 if (!Disposed)
13 {
14 //如果該類的狀態參數有效則進行一系列操作
15 //否則以下的語句就會出錯
16 ......
17 ......
18 ......
19 .....
20 .....
21 }
22 }
23 }
24
25 public void Closed()
26 {
27 lock(LockObject)
28 {
29 if (!Disposed)
30 {
31 //如果該類的狀態參數有效則進行一系列清理操作
32 //否則以下的語句就會出錯
33
34 //并設置狀態參數為true
35 .......
36 ......
37 .......
38 .......
39 ........
40 .........
41 Disposed = true;
42
43 }
44 }
45 }
46 }
增加了Lock鎖此時線程安全了,這是最簡單的線程數據讀寫安全的方法,但是一個問題出來了Function1方法的使用是很頻繁的(比如異步消息接收),Close方法僅在不使用該類時調用一次,也就是說為了同步Disposed使得Function1在每次調用時都要等待上次調用結束才能進行否則就阻塞在Lock語句中,這樣一來多線程的優勢就完全喪失了.那么該如何才能保持多線程的優勢而又能使Disposed得到同步呢,采用讀寫鎖,也就是說只要存在讀鎖沒有釋放寫鎖的獲取就一直阻塞直到所有讀鎖都釋放,而只要有一個寫鎖沒有釋放所有鎖(不管讀還是寫)的獲取都要一直阻塞直到寫鎖釋放.總的來說讀瑣和寫鎖獲取的邏輯條件如下:
成功獲取讀鎖的充要條件是沒有任何寫鎖.
成功獲取寫鎖的充要條件是沒有任何鎖.
解決方案:設計一個類實現讀寫鎖獲取的充要條件,并且為了使用簡捷考慮返回一個實現IDispose接口并且能指示是否成功獲取的屬性,如下面的樣子:
1 /// <summary>
2 /// 指示某種狀態接口
3 /// 本接口一般用在其它對象鎖定方法中的返回值:
如IReadWriteLock接口方法中的返回值
4 /// 使用using將使在using塊中鎖定本接口的當前狀態
5 /// 調用該接口的IDisposable.Dispose()釋放狀態鎖定
6 /// </summary>
7 public interface IDisposeState : IDisposable
8 {
9 /// <summary>
10 /// 是否有效狀態
11 /// </summary>
12 bool IsValid { get; }
13 }
該接口的IsValid屬性指示是否成功獲取鎖.設計實現讀寫鎖的類實現類似以下的接口已滿足讀寫鎖的獲取的邏輯要求:
1 /// <summary>
2 /// 讀寫鎖定接口
3 /// </summary>
4 public interface IReadWriteLock
5 {
6 /// <summary>
7 /// 獲取讀鎖
8 /// </summary>
9 /// <param name="timeout">超時值:TimeSpan.MaxValue指示無限等待</param>
10 /// <returns>IDisposeState:調用該接口的IDisposable.Dispose()
釋放狀態鎖定</returns>
11 IDisposeState LockRead(TimeSpan timeout);
12 /// <summary>
13 /// 獲取寫鎖
14 /// </summary>
15 /// <param name="timeout">超時值:
TimeSpan.MaxValue指示無限等待</param>
16 /// <returns>IDisposeState:
調用該接口的IDisposable.Dispose()釋放狀態鎖定</returns>
17 IDisposeState LockWrite(TimeSpan timeout);
18 }
該接口的讀寫鎖獲取函數都返回前面定義的IDisposeState接口,對該接口的使用方法如下:
1 public class DIO
2 {
3
4 private bool Disposed = false;
5
6 private IReadWriteLock LockObject = new ReadWriteLock();
7
8 private TimeSpan TimeOut = new TimeSpan(0, 0, 10);
9
10 public void Function1()
11 {
12 using(IDisposeState y = LockObject.LockRead(TimeOut))
13 {
14 if(!y.IsValid)return;
15 if (!Disposed)
16 {
17 //如果該類的狀態參數有效則進行一系列操作
18 //否則以下的語句就會出錯
19 ......
20 ......
21 ......
22 .....
23 .....
24 }
25 }
26 }
27
28 public void Closed()
29 {
30 using(IDisposeState y = LockObject.LockRead(TimeOut))
31 {
32 if(!y.IsValid)return;
33 if (!Disposed)
34 {
35 //如果該類的狀態參數有效則進行一系列清理操作
36 //否則以下的語句就會出錯
37
38 //并設置狀態參數為true
39 .......
40 ......
41 .......
42 .......
43 ........
44 .........
45 Disposed = true;
46
47 }
48 }
49 }
50 }
這樣上面的那個例子類就在同步了Disposed參數的同時保持了多線程的優勢了.
實現要點:
步驟一 鎖定內部資源(排它鎖)
步驟二 判斷讀寫鎖邏輯是否滿足,如果滿足則進行鎖登記等等操作
步驟三 解除排它鎖
步驟四 如果步驟二滿足則返回有效鎖,否則線程隨機停頓一段時間后重新執行步驟一直到成功或超時
這里的關鍵點是排它鎖的獲取,由于它是不停的輪值詢問使用的,所以它的實現要求使用資源少且速度快.
參考如下兩個類,一個使用Lock,一個沒有使用
1 internal sealed class LockLock
2 {
3
4 private bool g_Locked;
5
6 private object g_LockObj = new object();
7
8 public bool Lock()
9 {
10 lock (g_LockObj)
11 {
12 if (!g_Locked)
13 {
14 g_Locked = true;
15 return true;
16 }
17 else
18 return false;
19 }
20 }
21
22 public bool UnLock()
23 {
24 lock (g_LockObj)
25 {
26 if (g_Locked)
27 {
28 g_Locked = false;
29 return true;
30 }
31 else
32 return false;
33 }
34 }
35 }
不使用Lock
1 internal sealed class IntLock
2 {
3
4 public IntLock()
5 {
6 //初始化為0
7 //沒有鎖
8 g_Radom = 0;
9 }
10
11 //等于0指示沒有鎖,此時Lock方法應該返回成功(True)
12 //等于1說明存在鎖此時Lock方法應該返回失敗(False)
13 private int g_Radom;
14
15 public bool Lock()
16 {
17 //原子比較方法
18 //如果g_Radom等于0則替換為1且返回0,否則它是返回1的
19 return Interlocked.CompareExchange(
20 ref g_Radom, 1, 0) == 0;
21 }
22
23 public bool UnLock()
24 {
25 //原子比較方法
26 //如果g_Radom等于1則替換為0且返回1,否則它是返回0的
27 return Interlocked.CompareExchange(
28 ref g_Radom, 0, 1) == 1;
29 }
30 }
這兩個類都實現了排它鎖的功能,都可以用在步驟一和三,由于該鎖使用極其頻繁所以我們比較一下這兩個類的性能看看:
分別對這兩個類循環調用Lock或UnLock方法得出如下結果
調用次數 Lock方法耗時(毫秒) UnLock方法耗時(毫秒)
IntLock類 100000000 3390.625 3421.875
LockLock類 7000 7078.125
IntLock類 10000000 343.75 343.75
LockLock類 703.125 671.875
IntLock類 1000000 31.25 31.25
LockLock類 62.5 62.5
IntLock類 100000 0 0
LockLock類 15.625 15.625
從以上的結果看出IntLock類要比使用Lock的類(LockLock)速度要快一倍以上,所以應該采用IntLock這樣的方案來構造排它鎖的類.
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉載自:博客園