首页 > 其他分享 >.NET Core多线程 (4) 锁机制

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

时间:2023-08-11 09:33:40浏览次数:44  
标签:Core lock 线程 NET 多线程 CPU

合集:.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》

 

作者:周旭龙

出处:https://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

标签:Core,lock,线程,NET,多线程,CPU
From: https://www.cnblogs.com/edisonchou/p/dotnet_multithread_learning_notes_chap4.html

相关文章

  • Stable Diffusion基础:ControlNet之人体姿势控制
    在AI绘画中精确控制图片是一件比较困难的事情,不过随着ControlNet的诞生,这一问题得到了很大的缓解。今天我就给大家分享一个使用StableDiffusionWebUI+OpenPoseControlNet复制照片人物姿势的方法,效果可以参考上图。OpenPose可以控制人体的姿态、面部的表情,有时候还能......
  • ASP.NET Core – View Component
    前言以前写过 Asp.netcore学习笔记(ViewComponent组件),这篇作为翻新版. 参考Docs– ViewcomponentsinASP.NETCoreDon'treplaceyourViewComponentswithRazorComponents (RazorComponent无法替代ViewComponent) 介绍ViewComponent是 Partia......
  • .NET对象的内存布局
    在.NET中,理解对象的内存布局是非常重要的,这将帮助我们更好地理解.NET的运行机制和优化代码,本文将介绍.NET中的对象内存布局。.NET中的数据类型主要分为两类,值类型和引用类型。值类型包括了基本类型(如int、bool、double、char等)、枚举类型(enum)、结构体类型(struct),它们直接存储值。......
  • CAN转PN网关profinet通讯协议与D
    你是否曾经遇到过这样的问题:如何将各种CAN设备连接到PROFINet网络中?捷米JM-PN-CAN通讯网关或许能为你解决这个难题!捷米JM-PN-CAN网关是一款自主研发的通讯网关,具有将从站功能发挥到极致。它能够将各种CAN设备轻松接入到PROfinet网络中,让你的设备实现更加高效、稳定的通信......
  • Asp.Net Core 之 @Html.Action 迁移
    想必只要接触了netcore的小伙伴们已经发现@html.Action()方法官方已经不提供支持了,转而使用 ViewComponents替代了,同时也增加了TagHelper。但是如果想用以前的@Html.Action()方法,我们其实可以自己动手去实现它。下面就开始实现之旅吧!1、创建静态类 HtmlHelperViewExt......
  • .net6 过滤器、管道模型
    管道处理模型1、[中间件](https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-7.0)可以在典型应用中了解现有中间件的顺序,以及在哪里添加自定义中间件。你可以完全控制如何重新排列现有中间件,或根据场景需要注入新的自定义中间件。......
  • .NET5从零基础到精通:全面掌握.NET5开发技能【第三章】
    章节第一章:https://www.cnblogs.com/kimiliucn/p/17613434.html第二章:https://www.cnblogs.com/kimiliucn/p/17620153.html第三章:https://www.cnblogs.com/kimiliucn/p/17620159.html十三、权限验证13.1-基于Seesion/Cookies的权限认证为了拦截一些操作:传统的授权方式:S......
  • solr的master-slave和Multiple Cores
    Solrmulticore配置April21st,2011绚丽也尘埃LeaveacommentGotocommentsSolr继续学习中,感觉Solr的multicore主要用途有两个:1、充分利用服务器资源。在一台服务器上部署不用的搜索应用。2、提高一个应用服务能力,在服务器上同时部署同一个应用的多个core,这些core共用一份索......
  • CompletableFuture异步多线程
    importjava.util.concurrent.CompletableFuture;importjava.util.concurrent.ExecutionException;publicstaticvoidmain(String[]args)throwsInterruptedException,ExecutionException{longstartTime=System.currentTimeMillis();//调用用户服......
  • ASP.NET 使用Ajax
    本文将介绍在ASP.NET中如何方便使用Ajax,第一种当然是使用jQuery的ajax,功能强大而且操作简单方便,第二种是使用.NET封装好的ScriptManager。$.ajax向普通页面发送get请求这是最简单的一种方式了,先简单了解jQueryajax的语法,最常用的调用方式是这样:$.ajax({settings});有几个常用......