天天看點

一個線程阻塞的讀寫對象 .NET

 先說一說這個需求:

我有一個公共對象,(可能會拓展成一堆),有許多線程需要通路它,操作方式包括讀取和修改兩種,

我涉及到一個同步問題,就是,當有線程讀取時,其他的讀線程可以正常通路,而寫線程需要阻塞等待,

到無線程繼續讀時,才能開始寫(當然他阻塞的時候,不能允許新讀線程進入),

而當寫線程在通路對象時,其他的讀和寫線程都需要被阻塞.

我覺得這個問題比較難的地方就是,有時候是需要互斥所有線程,有時候隻互斥寫線程,

如果是互斥所有線程,我們的對象設計可以這麼簡單就實作.

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);

繼續閱讀