- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
原子操作,指一段逻辑要么全部成功,要么全部失败。概念上类似数据库事物(Transaction). CPU能够保证单条汇编的原子性,但不保证多条汇编的原子性 那么在这种情况下,那么CPU如何保证原子性呢?CPU本身也有锁机制,从而实现原子操作 。
int location = 10;
location++;
Interlocked.Increment(ref location);
常规代码不保证原子性 使用Interlocked类,底层使用CPU锁来保证原子性 。
CPU lock前缀保证了操作同一内存地址的指令不能在多个逻辑核心上同时执行 。
思考一个问题,x86架构(注意不是x86-64)的位宽是32位,寄存器一次性最多只能读取4byte数据,那么面对long类型的数据,它能否保证原子性呢? 可以看到,x86架构面对8byte数据,分为了两步走,首先将低8位FFFFFFFF赋值给exa寄存器,再将高8位的7FFFFFFF赋值给edx寄存器。最后再拼接起来。而面对4byte的int,则一步到位。 前面说到,多条汇编CPU不保证原子性,因此当x86架构面对超过4byte的数据不保证原子性.
要解决此类尴尬情况,要么使用64位架构,要么利用CPU的锁机制来保证原子性。 C#中的Interlocked.Read为了解决long类型的尴尬而生,是一个利用CPU锁很好的例子 。
操作系统中锁分为两种,用户态锁(user-mode)和内核态锁(kernel-mode).
在 C# 中,volatile是一个关键字,用于修饰字段。它告诉编译器和运行时环境,被修饰的字段可能会被多个线程同时访问,并且这些访问可能是异步的。这意味着编译器不能对该字段进行某些优化,以确保在多线程环境下能够正确地读取和写入这个字段的值 。
//例子1
static class StrangeBehavior
{
private static bool s_stopWorker = false;
public static void Run()
{
Thread t = new Thread(Worker);
t.Start();
Thread.Sleep(5000);
s_stopWorker = true;//5秒之后,work方法应该结束循环
}
private static void Worker()
{
int x = 0;
while (!s_stopWorker)
{
x++;
}
Console.WriteLine($"worker:stopped when x={x}");//在release模式下,该代码不执行。陷入了死循环出不来
}
}
JIT编译优化的时候,发现while (!s_stopWorker)中的s_stopWorker在该方法中永远不会变。因此就自作主张直接生成了while(ture)来“优化”代码 。
//例子2
class MyClass
{
private int _myField;
public void MyMethod()
{
_myField = 5;
int localVar = _myField;
}
}
编译器认为_myField被赋值5后,不会被其它线程改变。所有它会_myFieId的值直接加载到寄存器中,而后续使用localVar时,直接从寄存器读取(CPU缓存),而不是再次从内存中读取。这种优化在单线程中是没有问题的,但在多线程环境下,会存在问题.
因此我们需要在变量前,加volatile关键字。来告诉编译器不要优化.
使用Interlocked实现一个最简单的自旋锁 。
public struct SpinLockSmiple
{
private int _useNum = 0;
public SpinLockSmiple()
{
}
public void Enter()
{
while (true)//一个死循环,如果锁竞争激烈就会占用CPU时间片
{
if (Interlocked.Exchange(ref _useNum, 1) == 0)
return;
}
}
public void Exit()
{
Interlocked.Exchange(ref _useNum, 0);
}
}
上面的自旋锁有一个很大问题,就是CPU会全力运算。使用CPU最大的性能。 实际上,当我们没有获取到锁的时候,完全可以让CPU“偷懒”一下 。
public struct SpinLockSmiple
{
private int _useNum = 0;
public SpinLockSmiple()
{
}
public void Enter()
{
while (true)
{
if (Interlocked.Exchange(ref _useNum, 1) == 0)
return;
Thread.SpinWait(10);;//让CPU偷个懒,不要这么努力的运行
}
}
public void Exit()
{
Interlocked.Exchange(ref _useNum, 0);
}
}
SpinWait函数在x86平台上会调用pause指令,pause指令实现一个很短的延迟空等待操作,这个指令的执行时间大概是10+个 CPU时钟周期。让CPU跑得慢一点.
Thread.SpinWait本质上是让CPU偷懒跑得慢一点,最多降低点功耗。并没有让出CPU时间片,所以治标不治本 因此可以使用SpinWait来进一步优化。 可以看到,在合适的情况下。SpinWait会让出当前时间片,以此提高执行效率。比Thread.SpinWait占着资源啥也不做强不少 。
SpinLock是C#提供的一种自旋锁,封装了管理锁状态和SpinWait.SpinOnce方法的逻辑,虽然做的事情相同,但是代码更健壮也更容易理解 其底层还是使用的SpinWait 。
事件(ManualResetEvent/AutoResetEvent)与信号量(Semaphores)是Windows内核中两种基元线程同步锁,其它内核锁都是在它们基础上的封装 。
Event锁有两种,分为ManualResetEvent\AutoResetEvent 。本质上是由内核维护的Int64变量当作bool来使,标识0/1两种状态,再根据状态决定线程等待与否.
需要注意的是,等待不是原地自旋,并不会浪费CPU性能。而是会放入CPU _KPRCB结构的WaitListHead链表中,不执行任何操作。等待系统唤醒 。
线程进入等待状态与唤醒可能会花费毫秒级,与自旋的纳秒相比,时间非常长。所以适合锁竞争非常激烈的场景 。
在Windows上Event对象通过CreateEventEx函数来创建,状态变化使用Win32 API ResetEvent/SetEvent 。
https://github.com/reactos/reactos/blob/master/sdk/include/xdk/ketypes.h 底层使用SignalState来存储状态 。
Semaphore的本质是由内核维护的Int64变量,信号量为0时,线程等待。信号量大于0时,解除等待。 它相对Event锁来说,比较特殊点是内部使用一个int64来记录数量(limit),举个例子,Event锁管理的是一把椅子是否被坐下,表状态。而Semaphore管理的是100把椅子中,有多少坐下,有多少没坐下,表临界点。拥有更多的灵活性.
在Windows上信号量对象通过CreateSemaphoreEx函数来创建,增加信号量使用ReleaseSemaphore,减少信号量使用WaitForMultipleObject 。
参考Event锁,它们内部共享同一个结构 。
Mutex是Event与Semaphore的封装,不做过多解读.
在Windows上,互斥锁通过CreateMutexEx函数来创建,获取锁用WaitForMultipleObjectsEx,释放锁用ReleaseMutex 。
用户态锁有它的好,内核锁有它的好。把两者合二为一有没有搞头呢?
混合锁是一种结合了自旋锁和内核锁的锁机制,在不同的情况下使用不同策略,明显是一种更好的类型.
Lock是一个非常经典且常用的混合锁,其内部由两部分构成,也分别对应不同场景下的用户态与内核态实现 。
Lock锁先使用用户态锁自旋一定次数,如果获取不到锁。再转换成内核态锁。从而降低CPU消耗.
Lock锁的原理是在对象的ObjectHeader上存放一个线程Id,当其它锁要获取这个对象的锁时,看一下有没有存放线程Id,如果有值,说明还被其他锁持有,那么当前线程则会短暂性自旋,如果在自旋期间能够拿到锁,那么锁的性能将会非常高。如果自旋一定次数后,没有拿到锁,锁就会退化为内核锁.
现在你理解了,为什么lock一定要锁一个引用类型吧?
private static object _lock = new object();
static void Main(string[] args)
{
lock (_lock)
{
Console.WriteLine($"id={Thread.CurrentThread.ManagedThreadId} in lock. ");
Debugger.Break();
}
Console.WriteLine($"id={Thread.CurrentThread.ManagedThreadId} out lock");
Debugger.Break();
}
private static object _lock = new object();
static void Main(string[] args)
{
Task.Run(Work);
Thread.Sleep(1000);
Debugger.Break();
Task.Run(Work);
Console.ReadLine();
}
static void Work()
{
Console.WriteLine($"current ManagedThreadId={Thread.CurrentThread.ManagedThreadId}");
lock (_lock)
{
Console.WriteLine($"id={Thread.CurrentThread.ManagedThreadId} in lock. ");
Thread.Sleep(int.MaxValue);//使第二个线程永远获取不到锁
}
}
首先自旋,然后自旋失败,转成内核锁,并用SyncBlock 来维护锁相关的统计信息,01代表SyncBlock的Index,08是一个常量,代表内核锁 。
基本上以Slim结尾的锁,都是混合锁。都是先自旋一定次数,再进入内核态。 比如ReaderWriterSlim,SemaphoreSlim,ManualResetEventSlim. 。
在C#中,SemaphoreSlim可以在一定程度上用于异步场景。它可以限制同时访问某个资源的异步操作的数量。例如,在一个异步的 Web 请求处理场景中,可以使用SemaphoreSlim来控制同时处理请求的数量。然而,它并不能完全替代真正的异步锁,因为它主要是控制并发访问的数量,而不是像传统锁那样提供互斥访问 。
https://github.com/StephenCleary/AsyncEx 大神维护了的一个异步锁的开源库,它将同步版的锁结构都做了一份异步版,弥补了.NET框架中的对异步锁支持不足的遗憾 。
即使是最快的锁,也数倍慢于没有锁的代码,因从CAS无锁算法应运而生。 无锁算法大量依赖原子操作,如比较并交换(CAS,Compare - And - Swap)、加载链接 / 存储条件(LL/SC,Load - Linked/Store - Conditional)等。以 CAS 为例,它是一种原子操作,用于比较一个内存位置的值与预期值,如果相同,就将该位置的值更新为新的值。 举个例子 。
internal class Program
{
public static DualCounter Counter = new DualCounter(0, 0);
static void Main(string[] args)
{
Task.Run(IncrementCounters);
Task.Run(IncrementCounters);
Task.Run(IncrementCounters);
Console.ReadLine();
}
public static DualCounter Increment(ref DualCounter counter)
{
DualCounter oldValue, newValue;
do
{
oldValue = counter;//1. 线程首先读取counter的当前值,存为oldvalue
newValue = new DualCounter(oldValue.A + 1, oldValue.B + 1);//2. 计算出新的值,作为预期值
}
while (Interlocked.CompareExchange(ref counter, newValue, oldValue) != oldValue);//3. 利用原子操作比较两者的值,如果操作失败,说明counter的值已经被其它线程修改,需要重新读取,直到成功。
return newValue;
}
public static void IncrementCounters()
{
var result = Increment(ref Counter);
Console.WriteLine("{0},{1}",result.A,result.B);
}
}
public class DualCounter
{
public int A { get; }
public int B { get; }
public DualCounter(int a,int b)
{
A = a;
B = b;
}
}
上面提到的无锁算法不一定比使用线程快。比如 。
简单汇总一下 。
最后此篇关于.NETCore锁(Lock)底层原理浅谈的文章就讲到这里了,如果你想了解更多关于.NETCore锁(Lock)底层原理浅谈的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
服务架构进化论 原始分布式时代 一直以来,我可能和大多数的人认知一样,认为我们的服务架构的源头是单体架构,其实不然,早在单体系
序列化和反序列化相信大家都经常听到,也都会用, 然而有些人可能不知道:.net为什么要有这个东西以及.net frameword如何为我们实现这样的机制, 在这里我也是简单谈谈我对序列化和反序列化的
内容,是网站的核心所在。要打造一个受用户和搜索引擎关注的网站,就必须从网站本身的内容抓起。在时下这个网络信息高速发展的时代,许多低质量的信息也在不断地充斥着整个网络,而搜索引擎对一些高质量的内容
从第一台计算机问世到现在计算机硬件技术已经有了很大的发展。不管是现在个人使用的PC还是公司使用的服务器。双核,四核,八核的CPU已经非常常见。这样我们可以将我们程序分摊到多个计算机CPU中去计算,在
基本概念: 浅拷贝:指对象的字段被拷贝,而字段引用的对象不会被拷贝,拷贝对象和原对象仅仅是引用名称有所不同,但是它们共用一份实体。对任何一个对象的改变,都会影响到另外一个对象。大部分的引用类型,实
.NET将原来独立的API和SDK合并到一个框架中,这对于程序开发人员非常有利。它将CryptoAPI改编进.NET的System.Security.Cryptography名字空间,使密码服务摆脱
文件与文件流的区别(自己的话): 在软件开发过程中,我们常常把文件的 “读写操作” ,与 “创造、移动、复制、删除操作” 区分开来
1. 前言 单元测试一直都是"好处大家都知道很多,但是因为种种原因没有实施起来"的一个老大难问题。具体是否应该落地单元测试,以及落地的程度, 每个项目都有自己的情况。 本篇为
事件处理 1、事件源:任何一个HTML元素(节点),body、div、button 2、事件:你的操作 &
1、什么是反射? 反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。 Oracle 官方对
1、源码展示 ? 1
Java 通过JDBC获得连接以后,得到一个Connection 对象,可以从这个对象获得有关数据库管理系统的各种信息,包括数据库中的各个表,表中的各个列,数据类型,触发器,存储过程等各方面的信息。
可能大家谈到反射面部肌肉都开始抽搐了吧!因为在托管语言里面,最臭名昭著的就是反射!它的性能实在是太低了,甚至在很多时候让我们无法忍受。不过不用那么纠结了,老陈今天就来分享一下如何来优化反射!&nbs
1. 前言 最近一段时间一直在研究windows 驱动开发,简单聊聊。 对比 linux,windows 驱动无论是市面上的书籍,视频还是社区,博文以及号主,写的人很少,导
问题:ifndef/define/endif”主要目的是防止头文件的重复包含和编译 ========================================================
不知不觉.Net Core已经推出到3.1了,大多数以.Net为技术栈的公司也开始逐步的切换到了Core,从业也快3年多了,一直坚持着.不管环境
以前面试的时候经常会碰到这样的问题.,叫你写一下ArrayList.LinkedList.Vector三者之间的区别与联系:原先一直搞不明白,不知道这三者之间到底有什么区别?哎,惭愧,基础太差啊,木
目录 @RequestParam(required = true)的误区 先说结论 参数总结 @RequestParam(r
目录 FTP、FTPS 与 SFTP 简介 FTP FTPS SFTP FTP 软件的主动模式和被动模式的区别
1、Visitor Pattern 访问者模式是一种行为模式,允许任意的分离的访问者能够在管理者控制下访问所管理的元素。访问者不能改变对象的定义(但这并不是强制性的,你可以约定为允许改变)。对管
我是一名优秀的程序员,十分优秀!