天天看点

一个线程阻塞的读写对象 .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);

继续阅读