C#多线程同步机制

1. 监视器(Monitor)和互斥锁(lock)

2. 读写锁(ReadWriteLock)

3. 系统内核对象 互斥对象(Mutex)

1) 互斥(Mutex), 信号量(Semaphore), 事件(AutoResetEvent/ManualResetEvent)

2) 线程池

自由锁(InterLocked)

 

对一个32位的整型数进行递增和递减操作来实现锁,有人会问为什么不用++或--来操作。因为在多线程中对锁进行操作必须是原子的,而++和--不具备这个能力。InterLocked类还提供了两个另外的函数Exchange, CompareExchange用于实现交换和比较交换。Exchange操作会将新值设置到变量中并返回变量的原来值: int oVal = InterLocked.Exchange(ref val, 1)。

 

监视器(Monitor)

在MSDN中对Monitor的描述是: Monitor 类通过向单个线程授予对象锁来控制对对象的访问。

Monitor类是一个静态类因此你不能通过实例化来得到类的对象。Monitor的成员可以查看MSDN,基本上Monitor的效果和lock是一样的,通过加锁操作Enter设置临界区,完成操作后使用Exit操作来释放对象锁。不过相对来说Monitor的功能更强,Moniter可以进行测试锁的状态,因此你可以控制对临界区的访问选择,等待or离开, 而且Monitor还可以在释放锁之前通知指定的对象,更重要的是使用Monitor可以跨越方法来操作。Monitor提供的方法很少就只有获取锁的方法Enter, TryEnter;释放锁的方法Wait, Exit;还有消息通知方法Pulse, PulseAll。经典的Monitor操作是这样的:

private static object Obj = false;
        // 通过监视器来创建临界区 
        static public string SendRcvMsg(SerialPort sp)
        {
            string rsp = "";
            try
            {
                // 等待线程进入 
                Monitor.Enter(Obj);
                sp.Write("cmd1");
                rsp = sp.ReadExisting();
                Monitor.Pulse(Obj);
            }
            finally
            {
                // 释放对象锁 
                Monitor.Exit(Obj);
            }
            return rsp;
        }
        // 通过属性设置整个方法为临界区 
        [MethodImpl(MethodImplOptions.Synchronized)]
        static public string SendRcvMsg2(SerialPort sp)
        {
            string rsp = "";
            try
            {
                sp.Write("cmd1");
                rsp = sp.ReadExisting();
            }
            finally
            {
                // 释放对象锁 
                Monitor.Exit(Obj);
            }
            return rsp;
        }
        //如果永远得不到锁那么程序就会进入死锁状态,我们可以采用Wait来解决,在调用Wait时加入超时时限就可以。 
        static public string SendRcvMsg3(SerialPort sp)
        {
            string rsp = "";
            if (Monitor.TryEnter(Obj))
            {
                Monitor.Wait(Obj, 1000);
                sp.Write("cmd1");
                rsp = sp.ReadExisting();
                Monitor.Pulse(Obj);
            }
            return rsp;
        }

 

互斥锁(lock)

lock关键字是实现线程同步的比较简单的方式,其实就是设置一个临界区。在lock之后的{...}区块为一个临界区,当进入临界区时加互斥锁,离开临界区时释放互斥锁。MSDN对lock关键字的描述是: lock 关键字可将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。


        static int counter = 0;
        static public void AddMinus(bool IsAdd)
        {
            lock (Obj)
            {
                if(IsAdd)
                {
                    counter++;
                }
                else counter--;
            }
        }

对lock的使用有几点建议:

对实例锁定lock(this),对静态变量锁定lock(typeof(val))。lock的对象访问权限最好是private,否则会出现失去访问控制现象。

读写锁(ReadWriteLock)

读写锁的出现主要是在很多情况下,我们读资源的操作要多于写资源的操作。但是如果每次只对资源赋予一个线程的访问权限显然是低效的,读写锁的优势是同时可以有多个线程对同一资源进行读操作。因此在读操作比写操作多很多,并且写操作的时间很短的情况下使用读写锁是比较有效率的。读写锁是一个非静态类所以你在使用前需要先声明一个读写锁对象:

static private ReaderWriterLock _rwlock = new ReaderWriterLock();

读写锁是通过调用AcquireReaderLock,ReleaseReaderLock,AcquireWriterLock,ReleaseWriterLock来完成读锁和写锁控制的

如果你想在读的时候插入写操作请使用UpgradeToWriterLock和DowngradeFromWriterLock来进行操作,而不是释放读锁。

有一点要注意的就是读锁和写锁的超时等待时间间隔的设置。通常情况下设置写锁的等待超时要比读锁的长,否则会经常发生写锁等待失败的情况。

系统内核对象 互斥对象(Mutex)

 

互斥对象的作用有点类似于监视器对象,确保一个代码块在同一时刻只有一个线程在执行。互斥对象和监视器对象的主要区别就是,互斥对象一般用于跨进程间的线程同步,而监视器对象则用于进程内的线程同步。互斥对象有两种:一种是命名互斥;另一种是匿名互斥。在跨进程中使用到的就是命名互斥,一个已命名的互斥就是一个系统级的互斥,它可以被其他进程所使用,只要在创建互斥时指定打开互斥的名称就可以。在.Net中互斥是通过Mutex类来实现。

其实对于OpenExisting函数有两个重载版本,

Mutex.OpenExisting (String)

Mutex.OpenExisting (String, MutexRights)

对于默认的第一个函数其实是实现了第二个函数 MutexRights.Synchronize|MutexRights.Modify操作。

由于监视器的设计是基于.Net框架,而Mutex类是系统内核对象封装了win32的一个内核结构来实现互斥,并且互斥操作需要请求中断来完成,因此在进行进程内线程同步的时候性能上要比互斥要好。

典型的使用Mutex同步需要完成三个步骤的操作:1.打开或者创建一个Mutex实例;2.调用WaitOne()来请求互斥对象;3.最后调用ReleaseMutex来释放互斥对象。


        static Mutex _mtx = new Mutex(false);
        static List<string> _resource = new List<string>();
        static public void AddString(string str)
        {
            // 设置超时时限并在wait前退出非默认托管上下文 
            if (_mtx.WaitOne(1000, true))
            {
                _resource.Add(str);
                _mtx.ReleaseMutex();
            }
        }

AutoResetEvent

一个AutoResetEvent象是一个"检票轮盘":插入一张通行证然后让一个人通过。"auto"的意思就是这个"轮盘"自动关闭或者打开让某人通过。线程将在调用WaitOne后进行等待或者是阻塞,并且通过调用Set操作来插入线程。如果一堆线程调用了WaitOne操作,那么"轮盘"就会建立一个等待队列。一个通行证可以来自任意一个线程,换句话说任意一个线程都可以通过访问AutoResetEvent对象并调用Set来释放一个阻塞的线程。

如果在Set被调用的时候没有线程等待,那么句柄就会一直处于打开状态直到有线程调用了WaitOne操作。这种行为避免了竞争条件-当一个线程还没来得急释放而另一个线程就开始进入的情况。因此重复的调用Set操作一个"轮盘"哪怕是没有等待线程也不会一次性的让所有线程进入。

WaitOne操作接受一个超时参数-当发生等待超时的时候,这个方法会返回一个false。当已有一个线程在等待的时候,WaitOne操作可以指定等待还是退出当前同步上下文。Reset操作提供了关闭"轮盘"的操作。AutoResetEvent能够通过两个方法来创建:

1.调用构造函数 EventWaitHandle wh = new AutoResetEvent (false); 如果boolean值为true,那么句柄的Set操作将在创建后自动被调用 ;

2. 通过基类EventWaitHandle方式 EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto); EventWaitHandle构造函数允许创建一个ManualResetEvent。人们应该通过调用Close来释放一个Wait Handle在它不再使用的时候。当在应用程序的生存期内Wait handle继续被使用,那么如果遗漏了Close这步,在应用程序关闭的时候也会被自动释放。


    class BasicWaitHandle
    {
        public AutoResetEvent endEvent = new AutoResetEvent(false);
        public void Dowork()
        {
            Random rd = new Random();
            int sleep = rd.Next(1000, 10000);
            Thread.Sleep(sleep);
            //....
            endEvent.Set(); // 唤醒 
        }


    }

    class DoWorks
    {
        public void Works()
        {
            List<AutoResetEvent> endEvents = new List<AutoResetEvent>();
            for (int i = 0; i < 10; i++)
            {
                BasicWaitHandle cl = new BasicWaitHandle();
                Thread th = new Thread(cl.Dowork);
                endEvents.Add(cl.endEvent);
                th.Start();
            }
            WaitHandle.WaitAll(endEvents.ToArray());
        }
    }

参考链接 https://www.cnblogs.com/gjhjoy/p/3568570.html

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@David Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值