先说一说这个需求:
我有一个公共对象,(可能会拓展成一堆),有许多线程需要访问它,操作方式包括读取和修改两种,
我涉及到一个同步问题,就是,当有线程读取时,其他的读线程可以正常访问,而写线程需要阻塞等待,
到无线程继续读时,才能开始写(当然他阻塞的时候,不能允许新读线程进入),
而当写线程在访问对象时,其他的读和写线程都需要被阻塞.
我觉得这个问题比较难的地方就是,有时候是需要互斥所有线程,有时候只互斥写线程,
如果是互斥所有线程,我们的对象设计可以这么简单就实现.
view plaincopy to clipboardprint?
using System;
using System.Collections.Generic;
using System.Text;
namespace ReadWriteObj
{
class MutexObj<T>
{
private object m_lock;
private T m_value;
public MutexObj(T val)
{
m_value = val;
m_lock = new object();
}
public T Value
get
{
lock (m_lock)
return m_value;
}
set
m_value = value;
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace ReadWriteObj
{
class MutexObj<T>
{
private object m_lock;
private T m_value;
public MutexObj(T val)
{
m_value = val;
m_lock = new object();
}
public T Value
get
{
lock (m_lock)
return m_value;
}
set
m_value = value;
}
}
为了能够便于测试用,稍微做了点修改,把进入和退入临界区分为单独的方法,修改后代码如下:
using System.Threading;
public void BeginRead()
Monitor.Enter(m_lock);
public void EndRead()
Monitor.Exit(m_lock);
public void BeginWrite()
public void EndWrite()
using System.Threading;
public void BeginRead()
Monitor.Enter(m_lock);
public void EndRead()
Monitor.Exit(m_lock);
public void BeginWrite()
public void EndWrite()
这样可以很简单且稳定的实现同步,却不能完全达到我们的需求.因为无法实现多个读线程同时访问.
看样子一个锁是很难实现这样的需求,我便构思了两个锁,一个用来负责同步读操作,一个用来同步写操作,
实际上我更喜欢把锁称之为门,那么我们就有了一个读门和写门,
这样,我用两扇门来描述的需求就成了:
当读线程访问时,要先把写门关闭,然后等待读门开启,进入读门后要,保持读门继续开启,读取完毕后将写门开启,
当写线程访问时,要先把读门关闭,然后等待写们开启,进入写门后立刻关闭写门,写完之后将读门和写门都打开.
想法是好的,于是动手写了,代码如下:
public class ReadWriteObj<T>
private EventWaitHandle m_readLock;
private AutoResetEvent m_writeLock;
public ReadWriteObj(T val)
m_readLock = new EventWaitHandle(true, EventResetMode.ManualReset);
m_writeLock = new AutoResetEvent(true);
get { return Value; }
set { Value = value; }
m_writeLock.Reset();
m_readLock.WaitOne();
m_writeLock.Set();
m_readLock.Reset();
m_writeLock.WaitOne();
m_readLock.Set();
public class ReadWriteObj<T>
private EventWaitHandle m_readLock;
private AutoResetEvent m_writeLock;
public ReadWriteObj(T val)
m_readLock = new EventWaitHandle(true, EventResetMode.ManualReset);
m_writeLock = new AutoResetEvent(true);
get { return Value; }
set { Value = value; }
m_writeLock.Reset();
m_readLock.WaitOne();
m_writeLock.Set();
m_readLock.Reset();
m_writeLock.WaitOne();
m_readLock.Set();
这段代码有一个显而易见的问题,就是当读线程通过读门进去之后,读门是没有关的,
所以读线程可以正常进去,但是每一个读线程读取完毕时,都进行了m_writeLock.Set()处理,
也就是把写门开启,这时,很可能还有读线程正在读取中,而就不应该把写门开启,放写线程进入.
所以这里必须要引入一个计数器,让最后退出的一个读线程去开启写门,修改代码后如下:
private int m_readCount = 0;
++m_readCount;
--m_readCount;
if(m_readCount==0)
m_writeLock.Set();
private int m_readCount = 0;
++m_readCount;
--m_readCount;
if(m_readCount==0)
m_writeLock.Set();
自己感觉好像没有问题了,就写了个程序来测试这个对象,
结果问题很快就出现,线程之间很容易出现读和写同时发生的情况,
仔细分析原因,我认为问题出在两个地方:
1.在EndRead()中,当我执行if判断m_readCount==0成立时,这时读门是开启的,
所以很可能在执行这句之后,在执行m_writeLock.Set()之前,这时有一个读线程进入了,
但紧接着,m_writeLock.Set()语句的执行又将写门开启,
从而导致读写同时进入.
2.在EndWrite()中,此时很可能读门和写门外都等着线程,当执行了m_readLock.Set()后,还没有来得及执行m_writeLock.Set()时,
一个读线程进入了,然后m_writeLock.Set()执行,写门又被打开,写线程也进入了.
那么我就只能在读门和写门进入时,做两次关门的动作了,修改代码如下:
继续测试,大部分情况是正确,但是当EndWrite()执行后,还是有可能出现读写同时的问题,
怎么解释呢?我是这么理解的,当写完时,我是需要将读门和写门都开放,总是有读和写都同时进来的可能性,
但是我又不能加上互斥锁,因为这样造成死锁的可能性很大,看来用这样的机制是不能解决问题的,
其根源在于我要同时开启两扇门.
我就想,那么我就不用两扇门了,让门一扇一扇的开,不是就回避掉这个问题了吗?
于是我就在一个互斥锁的基础上进行了扩展,用了一个两重门的机制,注意,是两重门,而不是两扇门,
怎么解释呢?
我的设计是,不管是读还是写,要想进入临界区,就需要通过两重门,而且这两重门都是会放过一个人后就自动关闭的,
这样,大家(读和写)都在第一道门外等候,当第一道门开启后,放进一个人,那人被卡在了第二道门外等候,这时有两种情况,
1.等候的是读,他会观察临界区内的人是读还是写,如果临界区是被读占据,好,自己人,等待区的读就自己打开第二道门,
然后自己进去,门关上了,他再开第一道门,放下一个人进等候区;那如果临界区是被写占据呢?没办法,只好在等候区等待写完毕.
2.等待的是写,那无论临界区是读还是写,我总是要等待,进入临界区后,我要把第一道门开启,放下一个进等待区.
关于上面第2点,可能有人要问,如果我临界区空闲呢,那时我应该让写自己打开第二道门自己进来.不用担心,
因为我会保证在读和写离开临界区时,开启第二道门,只有一种情况例外,就是读离开临界区,临界区再没有读线程(这里计数器还是需要的),
而等待区刚好一个读正在准备进来(因为时间片的原因,这种情况是可能发生的),这时候我不需要主动开启第二道门.
照着这个思想,编写代码如下:
class GetSetObj<T>
private AutoResetEvent m_first, m_second;
private int m_readCount;
private Op m_waitState, m_coreState;
public enum Op
GET,
SET,
IDLE
};
public GetSetObj(T val)
m_first = new AutoResetEvent(true);
m_second = new AutoResetEvent(true);
m_waitState = Op.IDLE;
m_coreState = Op.IDLE;
m_readCount = 0;
public Op GetCoreState()
return m_coreState;
get { return m_value; }
set { m_value = value; }
m_first.WaitOne();
m_waitState = Op.GET;
if (m_coreState != Op.SET)
m_second.Set();
m_second.WaitOne();
m_coreState = Op.GET;
m_first.Set();
if (m_readCount == 0)
if (m_waitState != Op.GET)
{
if (m_waitState == Op.IDLE)
{
m_coreState = Op.IDLE;
}
m_second.Set();
}
m_waitState = Op.SET;
m_coreState = Op.SET;
m_second.Set();
class GetSetObj<T>
private AutoResetEvent m_first, m_second;
private int m_readCount;
private Op m_waitState, m_coreState;
public enum Op
GET,
SET,
IDLE
};
public GetSetObj(T val)
m_first = new AutoResetEvent(true);
m_second = new AutoResetEvent(true);
m_waitState = Op.IDLE;
m_coreState = Op.IDLE;
m_readCount = 0;
public Op GetCoreState()
return m_coreState;
get { return m_value; }
set { m_value = value; }
m_first.WaitOne();
m_waitState = Op.GET;
if (m_coreState != Op.SET)
m_second.Set();
m_second.WaitOne();
m_coreState = Op.GET;
m_first.Set();
if (m_readCount == 0)
if (m_waitState != Op.GET)
{
if (m_waitState == Op.IDLE)
{
m_coreState = Op.IDLE;
}
m_second.Set();
}
m_waitState = Op.SET;
m_coreState = Op.SET;
m_second.Set();
好,测试程序跑起来,也没有发现问题,看来似乎是符合要求了.
附上测试程序的代码,供参考
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
public Form1()
InitializeComponent();
//private ReadWriteObj<string> m_obj = new ReadWriteObj<string>("");
private GetSetObj<string> m_obj = new GetSetObj<string>("");
private delegate void SetStr(Control con, string msg,Color colr);
private void SetMsg(Control con, string m, Color c)
if (this.InvokeRequired)
SetStr d = new SetStr(SetMsg);
this.Invoke(d, con, m, c);
else
con.Text = m;
con.BackColor = c;
private void Form1_Load(object sender, EventArgs e)
for (int i = 1; i <= 12; ++i)
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(ThreadProc), this.Controls["label" + i.ToString()]);
void ThreadProc(object o)
Control c = o as Control;
Random r = new Random();
while (true)
int temp = r.Next(1, 100);
string str = "";
if (temp <= 50)
str = c.Name + " Read";
SetMsg(c, str, Color.Yellow);
m_obj.BeginRead();
SetMsg(c, str, Color.Red);
SetMsg(labelObj, str, Color.Red);
temp = r.Next(1, 2) * 1000;
System.Threading.Thread.Sleep(temp);
SetMsg(labelObj, str, Color.Yellow);
m_obj.EndRead();
else
str = c.Name + " Write";
m_obj.BeginWrite();
SetMsg(c, str, Color.White);
SetMsg(labelObj, str, Color.White);
m_obj.EndWrite();
str = c.Name + " Sleep";
SetMsg(c, str, Color.Yellow);
temp = r.Next(6, 8) * 1000;
System.Threading.Thread.Sleep(temp);
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
public partial class Form1 : Form
public Form1()
InitializeComponent();
//private ReadWriteObj<string> m_obj = new ReadWriteObj<string>("");
private GetSetObj<string> m_obj = new GetSetObj<string>("");
private delegate void SetStr(Control con, string msg,Color colr);
private void SetMsg(Control con, string m, Color c)
if (this.InvokeRequired)
SetStr d = new SetStr(SetMsg);
this.Invoke(d, con, m, c);
else
con.Text = m;
con.BackColor = c;
private void Form1_Load(object sender, EventArgs e)
for (int i = 1; i <= 12; ++i)
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(ThreadProc), this.Controls["label" + i.ToString()]);
void ThreadProc(object o)
Control c = o as Control;
Random r = new Random();
while (true)
int temp = r.Next(1, 100);
string str = "";
if (temp <= 50)
str = c.Name + " Read";
SetMsg(c, str, Color.Yellow);
m_obj.BeginRead();
SetMsg(c, str, Color.Red);
SetMsg(labelObj, str, Color.Red);
temp = r.Next(1, 2) * 1000;
System.Threading.Thread.Sleep(temp);
SetMsg(labelObj, str, Color.Yellow);
m_obj.EndRead();
else
str = c.Name + " Write";
m_obj.BeginWrite();
SetMsg(c, str, Color.White);
SetMsg(labelObj, str, Color.White);
m_obj.EndWrite();
str = c.Name + " Sleep";
SetMsg(c, str, Color.Yellow);
temp = r.Next(6, 8) * 1000;
System.Threading.Thread.Sleep(temp);