浅解c#多线程读写锁(一)

  最近发表过一些对c#多线程数据读写安全线的文章,有网友说都是代码不好理解,我在这里就给出我的一些解释,希望大家多多指较.这里我重复一下多线程数据读写安全的观点:多线程下的数据安全应该指的是在使用数据的生存期内它是不变的,使用数据的生存期可以是一个过程或函数,当然这里的指的数据不包含过程或函数中的局部变量,因为局部变量它本身就是线程安全的数据.

  目标:确保数据在使用的生命期内是不变的.

  解决思路:对于数额的使用不外乎就是读和写,而读操作对数据是不会产生变化的,仅有写操作才对数据产生变化,试想一下两个线程同时对同一数据进行读和写操作,那么读出的数据很可能在使用过程中被写操作改变了,那么依据原先读出的数据进行的逻辑过程差之豪厘失之千理了.大家且看下面的一个例子,该例子的类有这么一个逻辑,它有一个状态变量Disposed,当它为False时该类的所有方法可用,当它为True时该类的所有方法不可用,如果使用就出现错误.而该类有一个方法Function1做一些操作,有一个方法Close关闭该类同时设置该类的状态变量为False.

View Code
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变量进行同步,那么对该类进行如下更改:

用Lock同步的类
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接口并且能指示是否成功获取的属性,如下面的样子:

IDisposeState接口
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属性指示是否成功获取锁.设计实现读写锁的类实现类似以下的接口已满足读写锁的获取的逻辑要求:

IReadWriteLock
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,一个没有使用

使用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

不使用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这样的方案来构造排它锁的类.

先说这些了,有空我再接下去说,请大家批评指正.

posted on 2011-05-26 12:50  悠竹客  阅读(9170)  评论(10编辑  收藏  举报

导航