• .NET Core多线程 (4) 锁机制


    合集:.NET Core多线程温故知新

     

    去年换工作时系统复习了一下.NET Core多线程相关专题,学习了一线码农老哥的《.NET 5多线程编程实战》课程,我将复习的知识进行了总结形成本专题。

    本篇,我们来复习一下.NET中锁机制的相关知识点,预计阅读时间10分钟。

    理解lock锁的底层原理

    (1)为什么要用锁?

    对某个共享代码区域(临界区)进行串行访问,使用lock来保证串行的安全

    (2)lock的用法

    lock (lockMe)
    {
       dict.Add(i.ToString(), DateTime.Now);
    }

    (3)lock的本质

    通过ILSpy反编译查看可以知道,lock是个语法糖,编译后其实是Monitor.Enter 和 Monitor.Exit 的封装

    复制代码
    try
    {
        Monitor.Enter(lockMe, ref lockTake);
    
        dict.Add(i.ToString(), DateTime.Now);
    }
    finally
    {
        if (lockTake)
        {
           Monitor.Exit(lockMe);
        }
    }
    复制代码

    (4)lock为何需要引用类型?

    首先,编译器要求lock中的所对象必须是引用类型。

    其次,因为lock会用到对象头中的同步块索引来进行同步,值类型没有堆中的数据。

    无锁化:线程的本地存储

    (1)线程本地存储

    static 的作用域在AppDomain下都可见,此时在多线程环境中,通过static共享变量的方式来同步,不可避免会出现锁竞争。如果能将作用域范围缩小,比如缩小到Thread级别,就可以避免锁竞争。例如:ConcurrentBag就是一个好的例子。

    (2).NET中的解决方案

    ThreadStatic(Attribute):当前线程拿到的是定义好的值,其他线程拿到的可能是默认值(值类型可能是0,引用类型可能是null,需要注意容错)。

    ThreadLocal:与ThreadStatic最大的区别在于ThreadStatic只在第一个线程初始化,ThreadLocal则会为每个线程初始化。

    (3)存储在哪里?

    • PEB 进程环境块
    • TEB 线程环境块
    • TLS 线程本地存储(Thread Local Storage),取决于一共有多少个DataSlot

    (4)应用场景

    用来做数据库连接池:DB连接池 基于 ThreadLocal实现,每个线程只能看见自己的请求队列;

    用来做链式追踪:比如Skywalking或Zipkin等,用到ThreadLocal做本地存储,记录完整的调用链条如:A -> B -> C -> D;

    内核态锁知多少

    (1)基于WaitHandle的内核锁

    这种锁是基于Windows底层的内核数据结构来维护线程之间的同步,比如:

    • AutoResetEvent / ManualResetEvent

    • Semaphore

    • Mutex

    (2)优缺点

    需要从用户态切换到内核态,相对来说比较重量级,相对耗费时间;内核模式的锁,不仅可用于创建线程同步,还可以创建进程同步。

    用户态锁知多少

    (1)用户态锁是啥?

    例如下面的代码:

    lock(obj)
    {
        ... // todo [1ms]
    }

    大部分都是在临界区进行等待时间很短(比如1ms)的加锁,能不能让thread在CLR或C#层面内旋(自旋)一下,从而提高性能呢?使用用户态锁就可以避免上下文切换和内核切换带来的高开销。

    (2)寻找解决方案

    保持线程在用户态又要尽可能少的消耗CPU时间

    时间片

      • Windows中一个时间片大概是30ms
      • Thread.Sleep(0)
        • 提前结束自己的时间片,然后把自己放入到就绪队列中,如果就绪队列中的线程优先级 >= Current Thread,那么其他线程会被调度
        • 如果就绪队列中的线程优先级 < Current Thread,那么Current Thread只能继续执行【低优先级线程得不到执行】
        • 整体CPU级别
      • Thread.Yield()
        • 提前结束自己的时间片,如果当前逻辑CPU上的就绪队列上有待执行的线程,那么这个线程就会被调度(不考虑优先级)【低优先级线程可以得到执行】
        • 逻辑CPU级别

    极端休眠时间

      • Sleep(1)
        • 本质上和Sleep(1000)一样,都需要休眠

    CAS原语

      • read, operate, write => 打包成原子性

    借助CLR内的AwareLock::SpinWait()

      • C# SpinWait
      • CLR SpinWait

    (3).NET内置的SpinLock(用户态)

    SpinLock在用法上和lock关键字差不多的。

    复制代码
    class Program
    {
       public static SpinLock spinLock = new SpinLock();
    
       public static int counter = 0;
    
       static void Main(string[] args)
       {
           Parallel.For(1, 1000001, (i) =>
           {
               var lockTaken = false;
               spinLock.Enter(ref lockTaken);
               ++counter;
               spinLock.Exit();
            }
       });
    
    
       Console.WriteLine($"counter={counter}");
    
       Console.ReadLine();
    }
    复制代码

    (4).NET CAS案例:Interlocked

    CPU直接操作的,主要用在一些简单类型上:

    • read

    • operation

    • write

    复制代码
    class Program
    {
            public static SpinLock spinLock = new SpinLock();
    
            public static int counter = 0;
    
            static void Main(string[] args)
            {
                Parallel.For(1, 1000001, (i) =>
                {
                    Interlocked.Increment(ref counter, 1);
                });
    
            Console.WriteLine($"counter={counter}");
    
            Console.ReadLine();
    }
    复制代码

    混合态锁知多少

    混合锁:用户态模式+内核态模式

    (1)ManualResetEventSlim

    它是如何实现的?

    • lock
    • ManualResetEvent
    • CAS
    • SpinWait(轻量级自旋锁)、SpinLock

    (2)SemaphoreSlim

    它是如何实现的?

    • ManualResetEvent + lock + SpinWait

    (3)ReaderWriterLockSlim

    这个锁的内核版是 ReaderWriterLock,不带Slim就代表是内核态的锁。

    这个锁顾名思义是读写锁,意思是:读可以并行,但写只能串行。EnterWriteLock() 需要等待所有的reader或writer锁结束,才能开始

    (4)CountdownEvent

    这个锁可以实现类似MapReduce的效果。

    它是如何实现的?

    基于ManualResetEvent事件做了底层封装。

    线程安全集合知多少

    (1)线程安全集合

    .NET中都有哪些线程安全的集合类型?

    ConcurrentBag  对应非线程安全类型:List

    ConcurrentQueue  对应非线程安全类型:Queue

    ConcurrentStack  对应非线程安全类型:Stack

    ConcurrentDictionary  对应非线程安全类型:Dictionary

    (2)BlockingCollection

    BlockingCollection 意为 阻塞集合。

    线程安全的集合 可以转换为 阻塞集合,只要它实现了IProducerConsumerCollection接口BlockingCollection可以实现类似发布订阅的业务场景应用:

    • 生产端Add进去发布的消息

    • 消费者端通过GetConsumingEnumerable()方法阻塞等待发布的消息

    ConcurrentDictonary的两个大坑

    (1)Values的坑

    • 观察现象

        • 业务场景:自己用ConcurrentDictionary封装了一个Cache

        • FullGC 将 LOH 上的对象回收了

          • 所有>=85000byte的都会被纳入LOH

    • 观察源码

        • Values方法每次都会生成一个新的List集合对象进行返回,每个对象都是大对象

    • 如何改进

        • 禁止调用Values方法

        • 借助lock + Dictionary实现类似操作避免每次生成新的List集合对象

    (2)GetOrAdd的坑

    • 观察现象

        • 业务场景:自己用ConcurrentDictionary封装了一个Redis连接池缓存

        • 借助GetOrAdd实现的CreateInstance方法未能实现线程安全导致连接池被大量反复创建

    • 观察源码

        • GetOrAdd方法中的valueFactory不是线程安全的

    • 如何改进

        • 借助Lazy改造字典的Value对象,保证创建方法只被执行一次,比如:将RedisConnection改为Lazy

    共享变量在Release模式下的Bug

    (1)现象

    同样的代码,通过共享变量控制工作线程是否要结束自己,在Debug模式下没有问题,但是在Release模式下有问题。

    (2)原因

    JIT提供了错误的决策导致CPU在解析代码时做了优化,将 共享变量 存放在了CPU的寄存器中。

    (3)WinDbg探究

    • Release模式

        • 查看memory中的共享变量的值

    • CPU寄存器

        • 查看共享变量的值

    (4)解决方案

    • 使用CancellationToken做取消

    • 不用Cache,都读内存address中的对象,性能会相对较低

        • 将共享变量 改为 易变结构,比如:private bool _shouldStop 改为 private volatile bool _shouldStop

    小结

    本篇,我们复习了锁机制相关的知识点。下一篇,我们将复习一下常见的.NET多线程相关的性能优化实践。

    参考资料

    一线码农,腾讯课堂《.NET 5多线程编程实战

    不明作者,《Task调度与await》

     

  • 相关阅读:
    nginx使用lua通过request_body按条件开放访问权限
    【LeetCode】摆动排序 [M](数组)
    深度学习应用篇-计算机视觉-图像分类[2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet模型结构、实现、模型特点详细介绍
    【计算机视觉 | 语义分割】语义分割常用数据集及其介绍(一)
    Vue05/Vue组件子传父、props校验、Vue父子组件传值总结
    yolo-目标检测算法简介
    ShardingJDBC配置读写分离
    【蓝桥杯选拔赛真题15】C++三个数排序 第十二届青少年组蓝桥杯C++选拔赛真题解析
    【行业动态】福建服装品牌如何完成差异化战略?
    PyTorch应用实战四:基于PyTorch构建复杂应用
  • 原文地址:https://www.cnblogs.com/edisonchou/p/dotnet_multithread_learning_notes_chap4.html