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

一、前言

  线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入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锁有以下建议

  1. 如果一个类的实例是public的,最好不要lock(this)。因为使用你的类的人也许不知道你用了lock,如果他new了一个实例,并且对这个实例上锁,就很容易造成死锁
  2. 如果MyType是public的,不要lock(typeof(MyType))
  3. 不要Lock一个字符串
3. System.Threading.Interlocked

  Interlocked类即互锁操作,是对某个内存位置的原子操作,在大多数计算机上,增加变量的操作都不是原子操作,需要以下三步完成:

  1. 将实例变量中的值加载到寄存器
  2. 在寄存器中加减该值
  3. 将加减后的值保存到实体变量中

  线程可能在执行完前两步时,被夺走了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对象的几种定义进行测试,分为一下几种情况

  1. 单实例化的多线程调用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
静态OKOK
非静态OKOK

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 多实例化的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
静态OKOK
非静态NGNG

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Lock的测试总结:
  1. 单实例的情况下,因为只有一个Object对象,所以不论是否静态,是否全局都可以单一访问,实现线程锁
  2. 多实例的情况下,因为静态的Obejct只有一个,所以不受实例化的影响,能够完成单一访问,实现线程锁
    但是对于非静态的Object,由于实例化了两个对象,如果对同一对象单一访问可以实现线程锁,如果分别访问两个对象,则不能实现线程锁的功能。
2. Monitor用法
System.Threading.Monitor.Enter(obj); //加锁
System.Threading.Monitor.Exit(obj); //解锁,释放资源
            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事件可以同时唤醒多个线程的作用便是如此。
在这里插入图片描述

测试使用的代码见链接:
链接: 资源包含内容如下图所示.
在这里插入图片描述

  • 22
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1.几种同步方法的区别 lockMonitor是.NET用一个特殊结构实现的,Monitor对象是完全托管的、完全可移植的,并且在操作系统资源要求方 面可能更为有效,同步速度较快,但不能跨进程同步。lockMonitor.Enter和Monitor.Exit方法的封装),主要作用是定临界区,使临 界区代码只能被获得的线程执行。Monitor.Wait和Monitor.Pulse用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成死 。 互斥体Mutex和事件对象EventWaitHandler属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模 式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。 互斥体Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热 闹。 EventWaitHandle 类允许线程通过发信号互相通信。 通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。 2.什么时候需要定 首先要理解定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。比如,最简单的情况是,一个计数器,两个线程 同时加一,后果就是损失了一个计数,但相当频繁的定又可能带来性能上的消耗,还有最可怕的情况死。那么什么情况下我们需要使用,什么情况下不需要 呢? 1)只有共享资源才需要定 只有可以被多线程访问的共享资源才需要考虑定,比如静态变量,再比如某些缓存中的值,而属于线程内部的变量不需要定。 2)多使用lock,少用Mutex 如果你一定要使用定,请尽量不要使用内核模块的定机制,比如.NET的Mutex,Semaphore,AutoResetEvent和 ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清 楚的了解到他们的不同和适用范围。 3)了解你的程序是怎么运行的 实际上在web开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑 定,当然对于ASP.NET中的Application对象中的数据,我们就要考虑加了。 4)把定交给数据库 数 据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源 头上的同步,我们多数的精力就可以集中在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库中同一条记录时,我们才考虑加。 5)业务逻辑对事务和线程安全的要求 这 条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲 一些性能,和很多的开发时间来做这方面的工作。而一般的应用中,许多情况下虽然程序有竞争的危险,我们还是可以不使用定,比如有的时候计数器少一多一, 对结果无伤大雅的情况下,我们就可以不用去管它。 3.InterLockedInterlocked 类提供了同步对多个线程共享的变量的访问的方法。如果该变量位于共享内存中,则不同进程的线程就可以使用该机制。互操作是原子的,即整个操作是不能由相 同变量上的另一个互操作所中断的单元。这在抢先多线程操作系统中是很重要的,在这样的操作系统中,线程可以在从某个内存地址加载值之后但是在有机会更改 和存储该值之前被挂起。
多线程编程中,资源共享是一个常见的问题。当多个线程同时访问和修改共享资源时,如果没有正确的同步机制,就会出现数据竞争和不可预测的结果。以下是一些处理多线程资源共享问题的常用方法: 1. 使用互斥Mutex)或lock):通过在访问共享资源的代码块上加,确保同一时间只有一个线程可以访问该资源。这样可以避免数据竞争和并发修改的问题。 ```csharp private static readonly object lockObject = new object(); lock (lockObject) { // 访问共享资源的代码 } ``` 2. 使用线程安全的集合类:在C#中,有一些线程安全的集合类,例如`ConcurrentQueue`、`ConcurrentStack`、`ConcurrentDictionary`等。它们内部实现了适当的同步机制,可以在多线程环境下安全地进行读写操作。 ```csharp ConcurrentQueue<int> queue = new ConcurrentQueue<int>(); // 线程1往队列中添加元素 queue.Enqueue(1); // 线程2从队列中取出元素 int item; if (queue.TryDequeue(out item)) { // 处理取出的元素 } ``` 3. 使用互斥体(Monitor):通过使用`Monitor`类来创建临界区,确保只有一个线程可以进入临界区访问共享资源。 ```csharp private static readonly object lockObject = new object(); Monitor.Enter(lockObject); try { // 访问共享资源的代码 } finally { Monitor.Exit(lockObject); } ``` 4. 使用原子操作:C#提供了一些原子操作的方法,例如`Interlocked`类的方法,可以在多线程环境下进行原子性的读写操作,避免数据竞争和并发修改的问题。 ```csharp private static int counter = 0; Interlocked.Increment(ref counter); // 原子性地增加计数器 ``` 以上方法可以帮助你处理多线程资源共享问题,确保线程安全和数据一致性。根据具体情况选择合适的方法来处理资源共享,以满足程序的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值