C#中的几种锁:用户模式锁、内核模式锁、动态计数、监视锁

C#中的几种锁:用户模式锁、内核模式锁、动态计数、监视锁

介绍几种C#中的锁,最常用的是最后的监视锁,其他的也有必要了解一下原理及应用,特别像WaitOne、WaitHandle在我们项目中应用还是挺多的。

文章目录

  • C#中的几种锁:用户模式锁、内核模式锁、动态计数、监视锁
    • 用户模式锁
    • 内核模式锁
    • 动态计数锁
    • 监视锁

锁:解决多线程中的数据共享安全问题。

用户模式锁

  • volatile关键字:取消release对底层的优化,在读写的时候都从内存中读取

  • SpinLock 旋转锁:

    SpinLock spinLock = new SpinLock();
    bool lockTaken = false;
    spinLock.Enter(ref lockTaken);
    spinLock.Exit();
    

内核模式锁

分为:事件锁、信号量、互斥锁、读写锁。

建议:通常不建议随便使用内核模式锁,资源付出相对较大。我们可以使用混合锁代替,以及我们马上讲到的lock关键字。

  • 事件锁(自动事件锁、手动事件锁):

    • 自动事件锁:AutoResetEvent

      AutoResetEvent myLock = new AutoResetEvent(true);//true:表示终止状态(初始状态),false表示非终止
      myLock.WaitOne();
      //...
      myLock.Set();
      
    • 手动事件锁:ManualResetEvent,和自动事件锁相比,差距在于可以对多个变量进行批量锁

      ManualResetEvent myLock = new ManualResetEvent(false);//true:可以正常通过的。false:拦截状态,禁止通过。
      
      myLock.WaitOne();//批量拦截
      //...//由于是一批,这里是无序的
      myLock.Set();
      
  • Semaphore 信号量

    • 基本原理:是通过int数值来控制线程的个数

    • Semaphore myLock = new Semaphore(5, 10);//第一个参数表示同时可以允许的线程数,第二个是最大值

    • Semaphore myLock = new Semaphore(1, 10);//每次只能一个线程通过

      Semaphore myLock = new Semaphore(1, 10);
      
      myLock.WaitOne();
      //...
      myLock.Release();
      
  • Mutex互斥锁

    可用于非全局变量互斥的情况,如同一ID的用户只允许提交一次抽奖请求。

Mutex mutex = new Mutex();

mutex.WaitOne();
//...
mutex.ReleaseMutex();

以上三种锁都有WaitOne()方法,因为他们都继承自waitHandle。

  • 读写锁ReaderWriterLock

    • 注意:读写锁并不是从限定线程个数的角度出发。而是按照读写的功能划分。

    • 读写锁的基本方案:多个线程可以一起读,只能让一个线程去写。

      模拟场景:多个线程读取,一个线程写。请思考:写的线程是否能够正常阻止读的线程?如果能阻止,则达到目标。

      static ReaderWriterLock readerWriterLock = new ReaderWriterLock();
      
      /// 
      /// 读取数据的线程
      /// 
      private static void ThreadRead()
      {
          while (true)
          {
              readerWriterLock.AcquireReaderLock(int.MaxValue);//参数:表示最大的超时时间
      
              Thread.Sleep(100);
              Console.WriteLine($"当前读取的tid={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
              readerWriterLock.ReleaseReaderLock();
          }
      }
      /// 
      /// 写入数据的线程
      /// 
      private static void ThreadWrite()
      {
          while (true)
          {
              readerWriterLock.AcquireWriterLock(int.MaxValue);//参数:表示最大的超时时间
      
              Thread.Sleep(3000);
              Console.WriteLine($"---------------------------------------------当前写入的tid={Thread.CurrentThread.ManagedThreadId}  {DateTime.Now.ToLongTimeString()}");
      
              readerWriterLock.ReleaseWriterLock();
          }
      }
      //通过观察,我们发现写入的时候,能够正常的拦截读取的线程。
      //PS:如果我们写入数据的任务耗时太长,比如十几秒或更长,此时读的线程会被卡死,从而超时。开发中要特别注意。
      
      

动态计数锁

  • CountdownEvent:限制线程数的一个机制,而且这个也是比较常用的(同属于信号量的一种).

  • 使用场景:基于多个线程从某一个表中读取数据:比如我们现有A、B、C…每一张数据表我们都希望通过多个线程去读取。因为用一个线程的话,那么数据量大会出现卡死的情况。

    举例:

    A表:10w数据–》10个线程读取,1个线程1w条数据。
    B表:5w数据 --》5个线程 1个线程1w
    C表:1w数据 --》2个线程 1个线程5k

private static CountdownEvent countdownEvent = new CountdownEvent(10);
//默认10个threadcount初始值,一个线程用一个就减掉1,直到为0后,相当于结束
static void LoadData()
{
    countdownEvent.Reset(10);//重置当前ThreadCount上限
    for (int i = 0; i < 10; i++)
    {
        Task.Factory.StartNew(() =>
                              {
                                  Thread.Sleep(500);
                                  LoadTableA();
                              });
    }

    //阻止当前线程,直到设置了System.Threading.CountdonwEvent为止
    countdownEvent.Wait();//相当于Task.WaitAll()

    Console.WriteLine("TableA加载完毕..........\r\n");

    //加载B表
    countdownEvent.Reset(5);
    for (int i = 0; i < 5; i++)
    {
        Task.Factory.StartNew(() =>
                              {
                                  Thread.Sleep(500);
                                  LoadTableB();
                              });
    }
    countdownEvent.Wait();
    Console.WriteLine("TableB加载完毕..........\r\n");

    //加载C表
    myLock7.Reset(2);
    for (int i = 0; i < 2; i++)
    {
        Task.Factory.StartNew(() =>
                              {
                                  Thread.Sleep(500);
                                  LoadTableC();
                              });
    }
    countdownEvent.Wait();
    Console.WriteLine("TableC加载完毕..........\r\n");
}

/// 
/// 加载A表
/// 
private static void LoadTableA()
{
    //在这里编写具体的业务逻辑...
    Console.WriteLine($"当前TableA正在加载中...{Thread.CurrentThread.ManagedThreadId}");
    countdownEvent.Signal();//将当前的ThreadCount--   操作,就是减掉一个值
}

/// 
/// 加载B表
/// 
private static void LoadTableB()
{
    //在这里编写具体的业务逻辑...
    Console.WriteLine($"当前TableB正在加载中...{ Thread.CurrentThread.ManagedThreadId}");
    countdownEvent.Signal();
}

/// 
/// 加载C表
/// 
private static void LoadTableC()
{
    //在这里编写具体的业务逻辑...
    Console.WriteLine($"当前TableC正在加载中...{Thread.CurrentThread.ManagedThreadId}");
    countdownEvent.Signal();
}

监视锁

Monitor 限制线程个数的一把锁。

  • lock 关键字,本质是Monitor的语法糖

    • 用ILSyp自己观察生成的代码,发现和Monitor是一样的
    • lock/Monitor的内部机制,本质上就是利用对上的“同步块”实现资源锁定。
    • PS:在前面挺多的锁中,只有Monitor有语法糖,说明这个重要。
  • Monitor

    • 锁住的资源一定要让可访问的线程能够访问到,所以不能是局部变量。
    • 锁住的资源千万不要是值类型。
    • lock 不能锁住string类型,虽然它是引用类型(这个可能存在疑问)。
    private static object syncRoot = new object();
    private int num;
    
    //【1】简单写法
    static void TestMethod1()
    {
        for (int i = 0; i < 100; i++)
        {
            Monitor.Enter(syncRoot);//锁住资源
            num++;
            Console.WriteLine(num);
            Monitor.Exit(syncRoot);//退出资源
        }
    }
    
    //【2】严谨的写法(更常用的写法)
    static void TestMethod2()
    {
        for (int i = 0; i < 100; i++)
        {
            bool taken = false;
            try
            {
                Monitor.Enter(syncRoot, ref taken);//这个类似于SpinLock
                num++;
                Console.WriteLine(num);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            finally
            {
                if (taken)
                {
                    Monitor.Exit(syncRoot);
                }
            }
        }
    }
    
    //总结:为了严谨性,保证程序正常秩序,我们在锁区域添加了异常处理,还要添加判断,非常麻烦。我们可以使用语法糖Lock。
    //语法糖:只是编译器层面的,底层代码生成还是跟以前一样的。
    static void Method11()
    {
        for (int i = 0; i < 100; i++)
        {
            lock (syncRoot)
            {
                num++;
                Console.WriteLine(num);
            }
        }
    }
    

你可能感兴趣的:(C#,c#,多线程,.net)