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

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

.NET Core多线程 (4) 锁机制的更多相关文章

  1. SDL 开发实战(七): SDL 多线程与锁机制

    为什么要用多线程?在音视频领域主要是实现音视频同步.实现了音视频同步,我们的播放器就基本上合格了. 这里我们将讲解一下SDL的多线程与锁机制. 多线程的好处主要是能使程序更加充分利用硬件(主要是CPU ...

  2. C 语言多线程与锁机制

    C 语言多线程与锁机制 多线程 #include <pthread.h> void *TrainModelThread(void *id) { ... pthread_exit(NULL) ...

  3. JAVA多线程与锁机制

    JAVA多线程与锁机制 1 关于Synchronized和lock synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码 ...

  4. Java多线程(二) 多线程的锁机制

    当两条线程同时访问一个类的时候,可能会带来一些问题.并发线程重入可能会带来内存泄漏.程序不可控等等.不管是线程间的通讯还是线程共享数据都需要使用Java的锁机制控制并发代码产生的问题.本篇总结主要著名 ...

  5. java多线程(三)——锁机制synchronized(同步语句块)

    用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法之行一个长时间的任务,那么B线程必须等待比较长的时间,在这样的情况下可以使用synchronized同步语句快来解 ...

  6. java多线程(二)——锁机制synchronized(同步方法)

    synchronized Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码.当两个并发线程访问同一个对象object中 ...

  7. java 多线程总结篇4——锁机制

    在开发Java多线程应用程序中,各个线程之间由于要共享资源,必须用到锁机制.Java提供了多种多线程锁机制的实现方式,常见的有synchronized.ReentrantLock.Semaphore. ...

  8. Lucene的多线程访问原则和同步,锁机制

    本文介绍lucene多线程环境下的使用原则和commit.lock与write.lock实现的锁机制. 设计之初就是服务于多线程环境,大多数情况下索引会被不至一个线程访问.索引时一个关键资源.在对这样 ...

  9. [java多线程] - 锁机制&同步代码块&信号量

    在美眉图片下载demo中,我们可以看到多个线程在公用一些变量,这个时候难免会发生冲突.冲突并不可怕,可怕的是当多线程的情况下,你没法控制冲突.按照我的理解在java中实现同步的方式分为三种,分别是:同 ...

  10. Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

    精彩理解:  https://www.jianshu.com/p/21be831e851e ;  https://blog.csdn.net/heyutao007/article/details/19 ...

随机推荐

  1. 2023-02-22:请用go语言调用ffmpeg,保存mp4文件的视频帧,每帧用ppm图片保存。

    2023-02-22:请用go语言调用ffmpeg,保存mp4文件的视频帧,每帧用ppm图片保存. 答案2023-02-22: 使用 github.com/moonfdd/ffmpeg-go 库. 先 ...

  2. 2022-06-29:x = { a, b, c, d }, y = { e, f, g, h }, x、y两个小数组长度都是4。 如果有: a + e = b + f = c + g = d + h

    2022-06-29:x = { a, b, c, d }, y = { e, f, g, h }, x.y两个小数组长度都是4. 如果有: a + e = b + f = c + g = d + h ...

  3. Prometheus采集Java程序指标信息

    采集Java程序JVM信息 创建 Spring Boot Application 应用程序 进行 https://start.spring.io 使用版本 Spring Boot v2.7.11和JD ...

  4. 人手一个 Midjourney,StableStudio 重磅开源!

    人手一个 Midjourney,StableStudio 重磅开源! Stability AI 公司在上个月 19 号推出了 Alpha 版本 StableLM 大语言模型,包含了 30 亿和 70 ...

  5. 一个.Net Core开发的开源动态壁纸软件

    推荐一个Github上Start超过10.8K的超火.好用.强大的.内置很多优美的动态壁纸软件. 项目简介 这是基于.Net Core+WPF开发的.开源的动态壁纸软件,壁纸设置支持任何文件形式,包括 ...

  6. 把vue组件发布到npm

    一直以来项目都使用他人开发的组件,于是乎自己也想倒腾着做一个,发布到npm 在其他项目里直接使用,这个组件上传和纯js 还是有一定区别的,在这个过程中也遇到了一些小问题,网上找了许多案例,都不是太全面 ...

  7. k8s calico网络

  8. 分布式机器学习(Parameter Server)

    分布式机器学习中,参数服务器(Parameter Server)用于管理和共享模型参数,其基本思想是将模型参数存储在一个或多个中央服务器上,并通过网络将这些参数共享给参与训练的各个计算节点.每个计算节 ...

  9. 一次查找分子级Bug的经历,过程太酸爽了

    "Debugging is like trying to find a needle in a haystack, except the needle is also made of hay ...

  10. AntV L7 快速入门示例

    1. 引言 L7 地理空间数据可视分析引擎是一种基于 WebGL 技术的地理空间数据可视化引擎,可以用于实现各种地理空间数据可视化应用.L7 引擎支持多种数据源和数据格式,包括 GeoJSON.CSV ...