【C#】各种锁

概述

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

一提到线程同步,就会提到锁,作为线程同步的手段之一,锁总是饱受质疑。一方面锁的使用很简单,只要在代码不想被重入的地方(多个线程同时执行的地方)加上锁,就可以保证无论何时,该段代码最多有一个线程在执行;另一方面,锁又不像它看起来那样简单,锁会造成很多问题:性能下降、死锁等。使用volatile关键字或者Interlocked中提供的方法能够避开锁的使用,但是这些原子操作的方法功能有限,很多操作实现起来很麻烦,如无序的线程安全集合。

用户模式锁

1、volatile 关键字

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

2、Spinlock 旋转锁

Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁则获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 :自旋锁不应该被长时间的持有(消耗 CPU 资源)。

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

内核模式锁

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

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

1、事件锁

自动事件锁:AutoResetEvent

WaitOne()进入等待,Set()会释放当前锁给一个等待线程。

var are = new AutoResetEvent(true);
are.WaitOne();
//...
are.Set();

手动事件锁:ManualResetEvent

WaitOne()进入等待,Set()会释放当前锁给所有等待线程。

var mre = new ManualResetEvent(false);
 
mre.WaitOne();//批量拦截,后续的省略号部分是无序执行的。
//...
mre.Set();//一次释放给所有等待线程

2、信号量

信号量:Semaphore

信号量可以控制同时通过的线程数以及总的线程数。

//第一个参数表示同时可以允许的线程数,比如1表示每次只允许一个线程通过,
//第二个是最大值,比如8表示最多有8个线程。
var semaphore = new Semaphore(1, 8);

3、互斥锁

互斥锁:Mutex

Mutex和Monitor很接近,但是没有Monitor.Pulse,Wait,PulseAll的唤醒功能,他的优点是可以跨进程,可以在同一台机器甚至远程机器人的不同进程间共用一个互斥体。

var mutex = new Mutex();
mutex.WaitOne();
//...
mutex.ReleaseMutex();

4、读写锁

读写锁:ReaderWriterLock

不要使用ReaderWriterLock,该类有问题(死锁、性能),请使用ReaderWriterLockSlim

.NET Framework有两个读取器-编写器锁,ReaderWriterLockSlim以及ReaderWriterLock。建议对所有新开发的项目使用 ReaderWriterLockSlim。 虽然 ReaderWriterLockSlim 类似于 ReaderWriterLock,但不同之处在于,前者简化了递归规则以及锁状态的升级和降级规则。ReaderWriterLockSlim避免了许多潜在的死锁情况。 另外,ReaderWriterLockSlim 的性能显著优于 ReaderWriterLock。

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

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

//源码摘录自微软官网
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
 
public class SynchronizedCache 
{
    
    
    private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
    private Dictionary<int, string> innerCache = new Dictionary<int, string>();
 
    public int Count
    {
    
     get {
    
     return innerCache.Count; } }
 
    public string Read(int key)
    {
    
    
        cacheLock.EnterReadLock();
        try
        {
    
    
            return innerCache[key];
        }
        finally
        {
    
    
            cacheLock.ExitReadLock();
        }
    }
 
    public void Add(int key, string value)
    {
    
    
        cacheLock.EnterWriteLock();
        try
        {
    
    
            innerCache.Add(key, value);
        }
        finally
        {
    
    
            cacheLock.ExitWriteLock();
        }
    }
 
    public bool AddWithTimeout(int key, string value, int timeout)
    {
    
    
        if (cacheLock.TryEnterWriteLock(timeout))
        {
    
    
            try
            {
    
    
                innerCache.Add(key, value);
            }
            finally
            {
    
    
                cacheLock.ExitWriteLock();
            }
            return true;
        }
        else
        {
    
    
            return false;
        }
    }
 
    public AddOrUpdateStatus AddOrUpdate(int key, string value)
    {
    
    
        cacheLock.EnterUpgradeableReadLock();
        try
        {
    
    
            string result = null;
            if (innerCache.TryGetValue(key, out result))
            {
    
    
                if (result == value)
                {
    
    
                    return AddOrUpdateStatus.Unchanged;
                }
                else
                {
    
    
                    cacheLock.EnterWriteLock();
                    try
                    {
    
    
                        innerCache[key] = value;
                    }
                    finally
                    {
    
    
                        cacheLock.ExitWriteLock();
                    }
                    return AddOrUpdateStatus.Updated;
                }
            }
            else
            {
    
    
                cacheLock.EnterWriteLock();
                try
                {
    
    
                    innerCache.Add(key, value);
                }
                finally
                {
    
    
                    cacheLock.ExitWriteLock();
                }
                return AddOrUpdateStatus.Added;
            }
        }
        finally
        {
    
    
            cacheLock.ExitUpgradeableReadLock();
        }
    }
 
    public void Delete(int key)
    {
    
    
        cacheLock.EnterWriteLock();
        try
        {
    
    
            innerCache.Remove(key);
        }
        finally
        {
    
    
            cacheLock.ExitWriteLock();
        }
    }
 
    public enum AddOrUpdateStatus
    {
    
    
        Added,
        Updated,
        Unchanged
    };
 
    ~SynchronizedCache()
    {
    
    
       if (cacheLock != null) cacheLock.Dispose();
    }
}
 
ReaderWriterLockSlim示例

动态计数

动态计数锁CountdownEvent

  • 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");
}

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

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

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

原子操作类:Interlocked

Interlocked类则提供了4种方法进行原子级别的变量操作。Increment , Decrement , Exchange 和CompareExchange 。

a、使用Increment 和Decrement 可以保证对一个整数的加减为一个原子操作。

b、Exchange 方法自动交换指定变量的值。

c、CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。

d、比较和交换操作也是按原子操作执行的。Interlocked.CompareExchange(ref a, b, c); 原子操作,a参数和c参数比较, 相等b替换a,不相等不替换。

监视锁

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# 锁汇总

猜你喜欢

转载自blog.csdn.net/weixin_44231544/article/details/131657030
今日推荐