首页 > 作文

C#的并发机制优秀在哪你知道么

更新时间:2023-04-04 23:28:06 阅读: 评论:0

目录
一行没用的代码却提高了效率?看似没用的invoke到底有什么用​深度解读,为何要加两把锁总结

笔者上次用c#写.net代码差不多还是10多年以前,由于当时java已经颇具王者风范,net几乎被打得溃不成军。因此当时笔者对于这个.net的项目态度比较敷衍了事,没有对其中一些优秀机制有很深的了解,在去年写《c和java没那么香了,高并发时代谁能称王》时都没给.net以一席之地,不过最近恰好机缘巧合,我又接手了一个windows方面的项目,这也让我有机会重新审视一下自己关于.net框架的相关知识。

项目原型要实现的功能并不复杂,主要就是记录移动存储设备中文件拷出的记录,而且需要尽可能少的占用系统资源,而在开发过程中我无意中加了一行看似没有任何效果的代码,使用invoke方法记录文件拷出情况,这样的操作却让程序执行效率明显会更高,这背后的原因特别值得总结。

一行没用的代码却提高了效率?

由于我需要记录的文件拷出信息并没有回显在ui的需要,因此也就没考虑并发冲突的问题,在最初版本的实现中,我对于filesystemwatcher的回调事件,都是直接处理的,如下:

private void deletefilehandler(object nder, filesystemeventargs e)        {            if(files.contains(e.fullpath))            {                files.remove(e.fullpath);               //一些其它操作            }        }

这个程序的处理效率在普通的办公pc上如果同时拷出20个文件,那么在拷贝过程中,u盘监测程序的cpu使用率大约是0.7%。

但是一个非常偶然的机会,我使用了event/delegate的invoke机制,结果发现这样一个看似的废操作,却让程序的cpu占用率下降到0.2%左右

 private void udiskwather_deleted(object nder, filesystemeventargs e)        { 鱼塘承包合同范本           if(this.invokerequired)            {                this.invoke(new deletedelegate(deletefilehandler), new object[] { nder,e });           周杰伦稻香歌词    }            el            {                deletefilehandler(nder, e);            }        }

在我最初的认识中.net中的delegate机制在调用过程中是要进行拆、装箱操作的,因此这不拖慢操作就不错了,但实际的验证结果却相反。​

看似没用的invoke到底有什么用

这里先给出结论,invoke能提升程序执行效率,其关键还是在于线程在多核之间切换的消耗要远远高于拆、装箱的资源消耗,我们知道我们程序的核心就是操作files这个共享变量,每次在被检测的u盘目录中如果发生文件变动,其回调通知函数可能都运行在不宁波的大学同的线程,如下:

invoke机制的背后其实就是保证所有对于files这个共享变量的操作,全部都是由一个线程执行完成的。

目前.net的代码都开源的,下面我们大致讲解一下invoke的调用过程,不管是begininvoke还是invoke背后其实都是调用的marshaledinvoke方法来完成的,如下:

​public iasyncresult begininvoke(delegate method, params object[] args) {            using (new multithreadsafecallscope()) {                control marshaler = findmarshalingcontrol();                return(iasyncresult)marshaler.marshaledinvoke(this, method, args, fal);            }        }​

marshaledinvoke的主要工作是创建threadmethodentry对象,并把它放在一个链表里进行管理,然后调用postmessage将相关信息发给要通信的线程,如下:

​private object marshaledinvoke(control caller, delegate method, object[] args, bool synchronous) {            if (!ishandlecreated) {                throw new invalidoperationexception(sr.getstring(sr.errornomarshalingthread));            }            activeximpl activeximpl = (activeximpl)properties.getobject(propactiveximpl);            if (activeximpl != null) {                intcurity.unmanagedcode.demand();            }            // we don't want to wait if we're on the same thread, or el we'll deadlock.            // it is important that syncsamethread always be fal for asynchronous calls.            //            bool syncsamethread = fal;            int pid; // ignored            if (safenativemethods.getwindowthreadprocessid(new handleref(this, handle), out pid) == safenativemethods.getcurrentthreadid()) {                if (synchronous)                    syncsamethread = true;            }            // store the compresd stack information from the thread that is calling the invoke()            // so we can assign the same curity context to the thread that will actually execute            // the delegate being pasd.            //            executioncontext executioncontext = null;            if (!syncsamethread) {                executioncontext = executioncontext.capture();            }            threadmethodentry tme = new threadmethodentry(caller, this, method, args, synchronous, executioncontext); 多是什么结构           lock (this) {                if (threadcallbacklist == null) {                    threadcallbacklist = new queue();                }            }            lock (threadcallbacklist) {                if (threadcallbackmessage == 0) {                    threadcallbackmessage = safenativemethods.registerwindowmessage(application.windowmessagesversion + "_threadcallbackmessage");                }                threadcallbacklist.enqueue(tme);            }            if (syncsamethread) {                invokemarshaledcallbacks();            }  el {                //                unsafenativemethods.postmessage(new handleref(this, handle), threadcallbackmessage, intptr.zero, intptr.zero);            }            if (synchronous) {                if (!tme.iscompleted) {          人有悲欢离合 月有阴晴圆缺          waitforwaithandle(tme.asyncwaithandle);                }                if (tme.exception != null) {                    throw tme.exception;                }                return tme.retval;            }            el {                return(iasyncresult)tme;            }        }​

invoke的机制就保证了一个共享变量只能由一个线程维护,这和go语言使用通信来替代共享内存的设计是暗合的,他们的理念都是“让同一块内存在同一时间内只被一个线程操作”。这和现代计算体系结构的多核cpu(smp)有着密不可分的联系,

这里我们先来科普一下cpu之间的通信mesi协议的内容。我们知道现代的cpu都配备了高速缓存,按照多核高速缓存同步的mesi协议约定,每个缓存行都有四个状态,分别是e(exclusive)、m(modified)、s(shared)、i(invalid),其中:

m:代表该缓存行中的内容被修改,并且该缓存行只被缓存在该cpu中。这个状态代表缓存行的数据和内存中的数据不同。

e:代表该缓存行对应内存中的内容只被该cpu缓存,其他cpu没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的数据与内存的数据一致。

i:代表该缓存行中的内容无效。

s:该状态意味着数据不止存在本地cpu缓存中,还存在其它cpu的缓存中。这个状态的数据和内存中的数据也是一致的。不过只要有cpu修改该缓存行都会使该行状态变成 i 。

四种状态的状态转移图如下:

​我们上文也提到了,不同的线程是有大概率是运行在不同cpu核上的,在不同cpu操作同一块内存时,站在cpu0的角度上看,就是cpu1会不断发起remote write的操作,这会使该高速缓存的状态总是会在s和i之间进行状态迁移,而一旦状态变为i将耗费比较多的时间进行状态同步。

因此我们可以基本得出this.invoke(newdeletedelegate(deletefilehandler), newobject[] { nder,e }); ;这行看似无关紧要的代码之后,无意中使files共享变量的维护操作,由多核多线程共同操作,变成了众多子线程向主线程通信,所有维护操作均由主线程进行,这也使最终的执行效率有所提高。

​深度解读,为何要加两把锁

在当前使用通信替代共享内存的大潮之下,锁其实是最重要的设计。

我们看到在.net的invoke实现中,使用了两把锁lock(this)lock(threadcallbacklist)

lock (this) {                if (threadcallbacklist == null) {                    threadcallbacklist = new queue();                }            }            lock (threadcallbacklist) {                if (threadcallbackmessage == 0) {                    threadcallbackmessage = safenativemethods.registerwindowmessage(application.windowmessagesversion + "_threadcallbackmessage");                }                threadcallbacklist.enqueue(tme);            }

在.net当中lock关键字的基本可以理解为提供了一个近似于cas的锁(compare and swap)。cas的原理不断地把”期望值”和”实际值”进行比较,当它们相等时,说明持有锁的cpu已经释放了该锁,那么试图获取这把锁的cpu就会尝试将”new”的值(0)写入”p”(交换),以表明自己成为spinlock新的owner。伪代码演示如下:

void cas(int p, int old,int new){    if *p != old        do nothing    el      *p ← new}

基于cas的锁效率没问题,尤其是在没有多核竞争的情况cas表现得尤其优秀,但cas最大的问题就是不公平,因为如果有多个cpu同时在申请一把锁,那么刚刚释放锁的cpu极可能在下一轮的竞争中获取优势,再次获得这把锁,这样的结果就是一个cpu忙死,而其它cpu却很闲,我们很多时候诟病多核soc“一核有难,八核围观”其实很多时候都是由这种不公平造成的。

为了解决cas的不公平问题,业界大神们又引入了tas(test and t lock)机制,个人感觉还是把tas中的t理解为ticket更好记一些,tas方案中维护了一个请求该锁的头尾索引值,由”head”和”tail”两个索引组成。

struct lockstruct{    int32 head;    int32 tail;} ;

“head”代表请求队列的头部,”tail”代表请求队列的尾部,其初始值都为0。

最一开始时,第一个申请的cpu发现该队列的tail值是0,那么这个cpu会直接获取这把锁,并会把tail值更新为1,并在释放该锁时将head值更新为1。

在一般情况下当锁被持有的cpu释放时,该队列的head值会被加1,当其他cpu在试图获取这个锁时,锁的tail值获取到,然后把这个tail值加1,并存储在自己专属的寄存器当中,然后再把更新后的tail值更新到队列的tail当中。接下来就是不断地循环比较,判断该锁当前的”head”值,是否和自己存储在寄存器中的”tail”值相等,相等时则代表成功获得该锁。

tas这类似于用户到政务大厅去办事时,首先要在叫号机取号,当工作人员广播叫到的号码与你手中的号码一致时,你就获取了办事柜台的所有权。

但是tas却存在一定的效率问题,根据我们上文介绍的mesi协议,这个lock的头尾索引其实是在各个cpu之间共享的,因此tail和head频繁更新,还是会引发调整缓存不停的invalidate,这会极大的影响效率。

因此我们看到在.net的实现中干脆就直接引入了threadcallbacklist的队列,并不断将tme(threadmethodentry)加入队尾,而接收消息的进程,则不断从队首获取消息.

lock (threadcallbacklist) {                if (threadcallbackmessage == 0) {                    threadcallbackmessage = safenativemethods.registerwindowmessage(application.windowmessagesversion + "_threadcallbackmessage");                }                threadcallbacklist.enqueue(tme);            }

当队首指向这个tme时,消息才被发送,其实是一种类似于mas的实现,当然mas实际是为每个cpu都建立了一个专属的队列,和invoke的设计略有不同,不过基本的思想是一致的。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注www.887551.com的更多内容!

本文发布于:2023-04-04 23:28:04,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/zuowen/900b727fdd0e26e738532cc51c3268fc.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

本文word下载地址:C#的并发机制优秀在哪你知道么.doc

本文 PDF 下载地址:C#的并发机制优秀在哪你知道么.pdf

标签:缓存   多核   操作   状态
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图