C#的并发机制优秀在哪?

【C#的并发机制优秀在哪?】作者 | 马超 责编 | 张红月
出品 | CSDN博客
上次用C#写.Net代码差不多还是10多年以前,由于当时JAVA已经颇具王者风范,.Net几乎被打得溃不成军 。因此当时笔者对于这个.Net的项目态度比较敷衍了事,没有对其中一些优秀机制有很深的了解,在去年写《C和Java没那么香了,高并发时代谁能称王》时都没给.Net以一席之地,不过最近恰好机缘巧合,我又接手了一个windows方面的项目,这也让我有机会重新审视一下自己关于.Net框架的相关知识 。
项目原型要实现的功能并不复杂,主要就是记录移动存储设备中文件拷出的记录,而且需要尽可能少的占用系统资源,而在开发过程中我无意中加了一行看似没有任何效果的代码,使用Invoke方法记录文件拷出情况,这样的操作却让程序执行效率明显会更高,这背后的原因特别值得总结 。
一行没用的代码却提高了效率?
由于笔者需要记录的文件拷出信息并没有回显在UI的需要,因此也就没考虑并发冲突的问题,在最初版本的实现中,我对于filesystemwatcher的回调事件,都是直接处理的,如下:
privatevoidDeleteFileHandler( objectsender, FileSystemEventArgs e ) { if(files.Contains(e.FullPath)) { files.Remove(e.FullPath); //一些其它操作 } }这个程序的处理效率在普通的办公PC上如果同时拷出20个文件,那么在拷贝过程中,U盘监测程序的CPU使用率大约是0.7% 。
但是一个非常偶然的机会,我使用了Event/Delegate的Invoke机制,结果发现这样一个看似的废操作,却让程序的CPU占用率下降到0.2%左右
privatevoidUdiskWather_Deleted( objectsender, FileSystemEventArgs e ) { if( this.InvokeRequired) { this.Invoke( newDeleteDelegate(DeleteFileHandler), newobject[] { sender,e }); } else { DeleteFileHandler(sender, e); } }在我最初的认识中.net中的Delegate机制在调用过程中是要进行拆、装箱操作的,因此这不拖慢操作就不错了,但实际的验证结果却相反 。
看似没用的Invoke到底有什么用
这里先给出结论,Invoke能提升程序执行效率,其关键还是在于线程在多核之间切换的消耗要远远高于拆、装箱的资源消耗,我们知道我们程序的核心就是操作files这个共享变量,每次在被检测的U盘目录中如果发生文件变动,其回调通知函数可能都运行在不同的线程,如下:

C#的并发机制优秀在哪?

文章插图
Invoke机制的背后其实就是保证所有对于files这个共享变量的操作,全部都是由一个线程执行完成的 。
C#的并发机制优秀在哪?

文章插图
目前.Net的代码都开源的,下面我们大致讲解一下Invoke的调用过程,不管是BeginInvoke还是Invoke背后其实都是调用的MarshaledInvoke方法来完成的,如下:
publicIAsyncResult BeginInvoke( Delegate method, paramsObject[] args ) { using( newMultithreadSafeCallScope) { Control marshaler = FindMarshalingControl; return(IAsyncResult)marshaler.MarshaledInvoke( this, method, args, false); } }MarshaledInvoke的主要工作是创建ThreadMethodEntry对象,并把它放在一个链表里进行管理,然后调用PostMessage将相关信息发给要通信的线程,如下:
privateObject MarshaledInvoke( Control caller, Delegate method, Object[] args, boolsynchronous ) { if(!IsHandleCreated) { thrownewInvalidOperationException(SR.GetString(SR.ErrorNoMarshalingThread)); } ActiveXImpl activeXImpl = (ActiveXImpl)Properties.GetObject(PropActiveXImpl); if(activeXImpl != null) { IntSecurity.UnmanagedCode.Demand; } // We don't want to wait if we're on the same thread, or else we'll deadlock. // It is important that syncSameThread always be false for asynchronous calls. // boolsyncSameThread = false; intpid; // ignored if(SafeNativeMethods.GetWindowThreadProcessId( newHandleRef( this, Handle), outpid) == SafeNativeMethods.GetCurrentThreadId) { if(synchronous) syncSameThread = true; } // Store the compressed stack information from the thread that is calling the Invoke // so we can assign the same security context to the thread that will actually execute // the delegate being passed. // ExecutionContext executionContext = null; if(!syncSameThread) { executionContext = ExecutionContext.Capture; } ThreadMethodEntry tme = newThreadMethodEntry(caller, this, method, args, synchronous, executionContext); lock( this) { if(threadCallbackList == null) { threadCallbackList = newQueue; } } lock(threadCallbackList) { if(threadCallbackMessage == 0) { threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage"); } threadCallbackList.Enqueue(tme); } if(syncSameThread) { InvokeMarshaledCallbacks; } else{ // UnsafeNativeMethods.PostMessage( newHandleRef( this, Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero); } if(synchronous) { if(!tme.IsCompleted) { WaitForWaitHandle(tme.AsyncWaitHandle); } if(tme.exception != null) { throwtme.exception; } returntme.retVal; } else{ return(IAsyncResult)tme; } }Invoke的机制就保证了一个共享变量只能由一个线程维护,这和Go语言使用通信来替代共享内存的设计是暗合的,他们的理念都是 "让同一块内存在同一时间内只被一个线程操作"。这和现代计算体系结构的多核CPU(SMP)有着密不可分的联系,


推荐阅读