C# 浅谈线程同步Lock、Monitor、Interlocked、Mutex等多种线程锁及测试汇总

C# 浅谈线程同步Lock、Monitor、Interlocked、Mutex等多种线程锁及测试汇总,第1张

C# 浅谈线程同步Lock、Monitor、Interlocked、Mutex等多种线程锁及测试汇总

https://blog.csdn.net/qq_42537006/article/details/104949841?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link
 

文章目录
一、前言
二、线程锁的类型
1. volatile关键字
2. Lock锁
3. System.Threading.Interlocked
4. Monitor
5. Mutex
6. ReaderWriterLock
7. 线程同步事件AutoResetEvent和ManualResetEvent
三、实例代码测试
1. Lock锁
Lock的测试总结:
2. Monitor用法
3. System.Threading.Interlocked
3. Mutex用法
4. ReaderWriterLock用法
5. 7, 同步事件AutoResetEvent和ManualResetEvent用法
一、前言
  线程不是一个计算机硬件的功能,而是 *** 作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要 *** 作系统投入CPU资源来运行和调度。
  线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。
  在讲述线程锁之前,我们先了解一下什么是线程同步?

  线程同步----在多线程程序中,会出现多个线程抢占一个资源的情况,这时间有可能会造成冲突,也就是一个线程可能还没来得及将更改的 资源保存,另一个线程的更改就开始了。可能造成数据不一致。因此引入多线程同步,也就是说多个线程只能一个对共享的资源进行更改,其他线程不能对数据进行修改。
  为了解决多个线程会占用一个资源的问题,我们会使用线程锁来解决。

二、线程锁的类型
  常用的线程锁分为一下七种:volatile关键字、Lock锁、System.Threading.Interlocked原子级别 *** 作、Monitor、Metux、ReaderWriterLock、EventWaitHandle同步事件。下面我们对这六种做详细的介绍:

1. volatile关键字
  volatile 并没有实现真正的线程同步, *** 作级别停留在变量级别并非原子级别,对于单系统处理器中,变量存储在主内存中,没有机会被别人修改。但是如果是多处理器,可能就会有问题,因为每个处理器都有单独的data cash,数据更新不一定立刻被写回到主存,可能会造成不同步

2. Lock锁
  Lock锁为 *** 作的代码块添加互斥对象,如果A线程正在访问,对象没有到达临界区,则B线程不会访问。不推荐使用Lock(this)的方式最为Lock锁,因为你不确定别是是否重新实例了含有Lock的对象。
对于Lock锁有以下建议

如果一个类的实例是public的,最好不要lock(this)。因为使用你的类的人也许不知道你用了lock,如果他new了一个实例,并且对这个实例上锁,就很容易造成死锁
如果MyType是public的,不要lock(typeof(MyType))
不要Lock一个字符串
3. System.Threading.Interlocked
  Interlocked类即互锁 *** 作,是对某个内存位置的原子 *** 作,在大多数计算机上,增加变量的 *** 作都不是原子 *** 作,需要以下三步完成:

将实例变量中的值加载到寄存器
在寄存器中加减该值
将加减后的值保存到实体变量中
  线程可能在执行完前两步时,被夺走了CPU的时间,另一个线程继续对同一个变量 *** 作,造成第一个线程继续执行加减 *** 作时, *** 作结果不准确。这是更微观的表现,
  Interlocked类则提供了4种方法进行原子级别的变量 *** 作。Increment , Decrement , Exchange 和CompareExchange 。使用Increment 和Decrement 可以保证对一个整数的加减为一个原子 *** 作。Exchange 方法自动交换指定变量的值。
  CompareExchange 方法组合了两个 *** 作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。比较和交换 *** 作也是按原子 *** 作执行的。
Interlocked.CompareExchange(ref a, b, c); 原子 *** 作,a参数和c参数比较, 相等b替换a,不相等不替换。

4. Monitor
  Monitor和Lock的方法类似,但是Monitor可以更好的保护功能块,通过Monitor.Enter可以占有对Obiect的使用权限,使用Monitor.Exit可以释放此权限,Monitor类同时提供了TryEnter(Object o,[int])的一个重载方法,该方法尝试获取o对象的独占权, ,当获取独占权失败时,将返回false。
  Lock可以当成Monitor封装后的方法,使用起来更简单方便,而且使用Monitor如果占有资源后报错,没有使用Finally调用Monitor.Exit释放资源,会导致其它线程锁死,所以使用Lock更为简单方便,Monitor还提供了三个静态方法Monitor.Pulse(Object o),Monitor.PulseAll(Object o)和Monitor.Wait(Object o ) ,用来实现一种唤醒机制的同步。

5. Mutex
  Mutex和Monitor很接近,但是没有Monitor.Pulse,Wait,PulseAll的唤醒功能,他的优点是可以跨进程,可以在同一台机器甚至远程机器人的不同进程间共用一个互斥体,当然也可以用于线程同步,不过因为它是win32封装的,所以他需要互 *** 作转换,会消耗更多的资源。

6. ReaderWriterLock
  如果我们仅仅是获取某个资源,并不会很频繁的对资源进行修改,那么占有获取权限则很浪费时间。那么net中的ReaderWriteLock提供了一种方法,当没有获得写的权限时,可以获得多个读的权限,当已经在执行写的权限时,则无法获得读取的权限,当写入完成之后,才能继续读取。

7. 线程同步事件AutoResetEvent和ManualResetEvent
  同:
    都是将布尔变量传递给构造函数控制初始状态,True则非堵塞,False则为堵塞
  异:
    虽然他们都可以使用Set()解除堵塞,但是AutoResetEvent每执行一次会自动Reset(),Autoevent.WaitOne()每次只允许一个线程。
ManualResetEvent则需要手动需要手动Reset(),所以Manualevent则可以唤醒多个线程,如果Manualevent.Set,信号被触发后,此事件则一直通路,直到手动Reset()才会继续堵塞

三、实例代码测试
  下面对以上几种方法中,比较常用的进行测试,并对比。

1. Lock锁
Lock主要针对Object对象的几种定义进行测试,分为一下几种情况

单实例化的多线程调用Lock
    Locker LockTest1 = new Locker("LockTest1");
    Thread GainMsg1 = new Thread(new ThreadStart(LockTest1.Work));
    GainMsg1.Name = "GainMsg1";
    Thread GainMsg2 = new Thread(new ThreadStart(LockTest1.Work));
    GainMsg2.Name = "GainMsg2";
    GainMsg1.Start();
    GainMsg2.Start();

全局Public    私有Private
静态    Public static Object = new object()    Private static Object = new object()
非静态    public Object obj = new object()    Private Object = new object()
测试结果:

全局Public    私有Private
静态    OK    OK
非静态    OK    OK

多实例化的Lock
    Locker LockTest1 = new Locker("LockTest1");
    Locker LockTest2 = new Locker("LockTest2");
    Thread GainMsg1 = new Thread(new ThreadStart(LockTest1.Work));
    GainMsg1.Name = "GainMsg1";
    Thread GainMsg2 = new Thread(new ThreadStart(LockTest2.Work));
    GainMsg2.Name = "GainMsg2";
    GainMsg1.Start();
    GainMsg2.Start();

全局Public    私有Private
静态    Public static Object = new object()    Private static Object = new object()
非静态    public Object obj = new object()    Private Object = new object()
测试结果:

全局Public    私有Private
静态    OK    OK
非静态    NG    NG

Lock的测试总结:
单实例的情况下,因为只有一个Object对象,所以不论是否静态,是否全局都可以单一访问,实现线程锁
多实例的情况下,因为静态的Obejct只有一个,所以不受实例化的影响,能够完成单一访问,实现线程锁
但是对于非静态的Object,由于实例化了两个对象,如果对同一对象单一访问可以实现线程锁,如果分别访问两个对象,则不能实现线程锁的功能。
2. Monitor用法
System.Threading.Monitor.Enter(obj); //加锁
1
System.Threading.Monitor.Exit(obj); //解锁,释放资源
1
            System.Threading.Monitor.Enter(obj);
            num = 0;
            for (int i = 0; i < 10; i++)
            {
                num += 1;
                msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
                Console.WriteLine(msg);
                Thread.Sleep(1000);
            }
            Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
            System.Threading.Monitor.Exit(obj);
 


3. System.Threading.Interlocked
  Increment:增量 +1
  Decrement :增量 -1
  Exchange :自动交换指定变量的值
  CompareExchange :比较两个值以及根据比较的结果将第三个值存储在其中一个变量中

3. Mutex用法
  线程使用Mutex.WaitOne()方法等待C# Mutex对象被释放,如果它等待的C# Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个C# Mutex对象的线程都只有等待。

    public void Work()
    {
        mutex.WaitOne();
        num = 0;
        for (int i = 0; i < 10; i++)
        {
            num += 1;
            msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
            Console.WriteLine(msg);
            Thread.Sleep(1000);
        }
        Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
        mutex.ReleaseMutex();
    }
 


4. ReaderWriterLock用法
 m_readerWriterLock.AcquireReaderLock(-1);//获取读取的权限,-1代表无限时等待,每次获取完毕后,记得释放.此权限可以被多线程同时获取,也就是“多读”。******如果不是释放,写入的权限将无法被获取**********
 
 m_readerWriterLock.ReleaseReaderLock();//释放读取权限资源
 
 m_readerWriterLock.AcquireWriterLock(-1);//获取写入的权限,-1代表无限时等待,每次写入完毕后,记得释放******如果不是放,其它线程将无法读取**********


        public void Read()
        {
                    
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireReaderLock");
                m_readerWriterLock.AcquireReaderLock(-1);
                num += 1;
                msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
                Console.WriteLine(msg);
                Thread.Sleep(1000);
                m_readerWriterLock.ReleaseReaderLock();
            }
            Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
           
        }
        public  void Writer()
        {

            Console.WriteLine("ThreadName " + Thread.CurrentThread.Name + " AcquireWriterLock");
            m_readerWriterLock.AcquireWriterLock(-1);
            num = 1000;
            msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
            Console.WriteLine(msg);
            Thread.Sleep(3000);
   
            Console.WriteLine("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name);
            m_readerWriterLock.ReleaseWriterLock();
        }

测试效果
  从打印信息可以看出。执行获取写入权限时,读取的权限无法被获取,会堵塞读取功能。反之如果读取的权限没有被释放,写入的权限也无法被获取。所以一定要养成用完即弃的习惯,避免死锁发生


5. 7, 同步事件AutoResetEvent和ManualResetEvent用法
  声明Auto和Manual事件时的起始布尔量设置为True,表示信号为通。设置为False信息为堵塞

public static EventWaitHandle WaitAutoTest = new AutoResetEvent(false);//线程
public static EventWaitHandle WaitManualTest = new ManualResetEvent(false);// 运动指令的堵塞

测试AutoResetEvent建立了两个单独的线程,方法分别是AutoTest1()、AutoTest2(),线程启动后,他们都会被Form1.WaitAutoTest.WaitOne();指令堵塞。

点击AutoResetEvent按钮后会执行 WaitAutoTest.Set();将信号置为通

       private void Btn_Auto_Click(object sender, EventArgs e)
        {
            WaitAutoTest.Set();
        }

此时只有一个线程会获得通路,然后会再次将信号置为False。当通路的线程运行完毕后会再次 执行WaitAutoTest.Set(),另一个线程则会通路。

        public void AutoTest1()
        {
            Form1.WaitAutoTest.WaitOne();
            int num = 0;
            for (int i = 0; i < 10; i++)
            {
                num += 1;
                msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
                if (dataReceive != null) dataReceive(msg);
                //Console.WriteLine(msg);
                Thread.Sleep(1000);
            }
            
            dataReceive("");
            dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
            dataReceive("");
            Form1.WaitAutoTest.Set();
        }

        public void AutoTest2()
        {
            Form1.WaitAutoTest.WaitOne();
            int num = 0;
            for (int i = 0; i < 10; i++)
            {
                num += 1;
                msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
                if (dataReceive != null) dataReceive(msg);
                Thread.Sleep(1000);
            }
         
            dataReceive("");
            dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
            dataReceive("");
            Form1.WaitAutoTest.Set();

        }

AutoResetEvent按钮测试结果如图
  可以看出执行一次WaitAutoTest.Set();只有一个线程获得了通路的机会,另个一个线程仍然处于堵塞状态。

测试ManualResetEvent同样建立了两个单独的线程,方法分别是ManualTest1()、ManualTest2(),线程启动后,他们都会被Form1.WaitManualTest.WaitOne();指令堵塞。


     private void Btn_Manual_Click(object sender, EventArgs e)
        {
            WaitManualTest.Set();
        }

点击ManualResetEvent按钮之后,会触发 WaitManualTest.Set();通线程唤醒。唤醒之后除非手动执行Reset().否则就会一直处于通路状态

    public void ManualTest1()
        {
            Form1.WaitManualTest.WaitOne();
            int num = 0;
            for (int i = 0; i < 10; i++)
            {
                num += 1;
                msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
                if (dataReceive != null) dataReceive(msg);
                //Console.WriteLine(msg);
                Thread.Sleep(1000);
            }
            
            dataReceive("");
            dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
            dataReceive("");
            Form1.WaitManualTest.Set();

        }

        public void ManualTest2()
        {
            Form1.WaitManualTest.WaitOne();
            int num = 0;
            for (int i = 0; i < 10; i++)
            {
                num += 1;
                msg = string.Format("线程 [{0}],实例[{1}]中num的值是[{2}]", Thread.CurrentThread.Name, this.Name, num);
                if (dataReceive != null) dataReceive(msg);
                Thread.Sleep(1000);
            }
           
            dataReceive("");
            dataReceive(string.Format("===线程[{0}] 执行完毕===", Thread.CurrentThread.Name));
            dataReceive("");
            Form1.WaitManualTest.Set();
        }

ManualResetEvent按钮测试结果如图
  可以看出执行一次WaitManualTest.Set();两个线程同时解除了堵塞,同时开始运行,所以ManualTest事件可以同时唤醒多个线程的作用便是如此。


测试使用的代码见链接:
链接: 资源包含内容如下图所示.

————————————————
版权声明:本文为CSDN博主「顶着太阳去烧烤」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_42537006/article/details/104949841

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5684253.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存