先說一說這個需求:
我有一個公共對象,(可能會拓展成一堆),有許多線程需要通路它,操作方式包括讀取和修改兩種,
我涉及到一個同步問題,就是,當有線程讀取時,其他的讀線程可以正常通路,而寫線程需要阻塞等待,
到無線程繼續讀時,才能開始寫(當然他阻塞的時候,不能允許新讀線程進入),
而當寫線程在通路對象時,其他的讀和寫線程都需要被阻塞.
我覺得這個問題比較難的地方就是,有時候是需要互斥所有線程,有時候隻互斥寫線程,
如果是互斥所有線程,我們的對象設計可以這麼簡單就實作.
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);