0%

进程、线程与协程 (C# vs Python)

近来由于项目需要,接触了一下一直没去了解过的Python异步语法,发现和之前我熟悉的C#有很多不同。在深入Python的异步逻辑之后,由于Python在语法上保留了很多语言机制的细节(比如成员函数的self参数),我反而对C#的异步有了更深的了解。这里就来重新梳理一下各种并行方法的区别,以及他们在C#和Python上实现的区别。(这里只讨论单机的并行机制。)

总的来说,并行机制主要有进程(Process)、线程(Thread)和协程(Coroutine),其并行实现的开销依次递减,但是他们对每个任务的鲁棒性也是依次递减的。进程是操作系统资源分配的最小单元,线程则是能够被CPU并行处理的最小单元,而协程则是目前实现“并行”的最简单方法。一个进程中可以有多个线程,而一个线程中可以有多个协程。他们具体在特性上有以下区别

进程线程协程
独立内存堆××
独立处理器(可硬件并行)×
独立上下文×
独立栈、寄存器状态

进程

进程是系统层面实现并行的机制了,进程管理是现代操作系统的一大核心之一。进程之间互不影响,操作系统会保证一个程序崩溃了,其他程序以及系统内核不会崩溃。操作系统还会提供其他的进程管理功能,例如进程调度、设置进程优先级等等。不同语言底层对进程接口的实现实际上都是对系统接口的封装。 ## 一些概念 与进程相关的概念通常都是操作系统课程的必修知识哈哈: - 进程间通信(Inter-process communiation, IPC):故名思意。常用手段有管道、共享内存、信号量(Semaphore)、消息队列等。 - 管道(Pipe):管道大概是进程间通信的最常用方式?分命名管道和匿名管道, 进程双方均可往其中读写数据。 - 远程过程调用(Remote procedure call): 远程过程调用通过特定的消息序列化手段,可以实现进程间通信,其使用形式是把一个“远程”的函数在本地进行执行。 - 进程锁:如果为了避免多个进程访问同一个资源的冲突的话,就会用到进程锁,其实现方法有管道、信号量、以及文件锁等。 - 文件锁:文件锁是实现进程互斥的一种常用手段,只需要建立空文件句柄并锁上就可了并且文件锁还能做到权限控制,非常方便 ## C# C#中对进程控制的模块主要通过System.Diagnostics.Process实现,可以实现建立进程、管理进程等,还可以指定具体的内存映射参数,如虚拟内存的页大小。而对管道的支持则是在Process类中有一部分,以及在System.IO.Pipe里面有更全面的接口。我觉得这样的命名空间分类是挺合理的,Process类的API其实只能用来进行程序调用和系统诊断,而Pipe则由于它和Stream的概念比较符合,因此归在IO空间下是合适的。 ## Python Python中对进程的控制以及通信方法的实现都在multiprocess包里,它的一些具体使用方法可以参考另一篇之前的博文。值得一提的是,Python中还针对Unix系统提供了fcntl, posix等库专门用来调用系统底层API,这些API有部分是和进程有关的。相关内容还是查阅对应的资料会比较清楚~

线程

线程是进程中细化的并行机制,线程的实现也需要用到操作系统的接口,不过线程的创建的管理基本都是在进程内部完成的。由于线程之间不独立内存空间,因此在C++这种能够随意操作内存的语言中,一个线程崩了,这个进程也大概率就崩了。但是在C#和Python中,由于有比较完善的Exception机制,并且没有什么机会直接操作内存,一般线程崩了主进程还是能接着跑的。多线程想必应该是大家用的最多的并行方法了~ ## 一些概念 在线程里面又有一些新的概念 - 线程池(Thread pool):线程池与内存池相似,都是为了避免频繁新建和销毁线程(or 内存)而造成额外的开销 - 线程锁:线程锁与进程锁相似,是为了避免线程间访问同样的资源而产生冲突(例如race condition)。线程间产生访问冲突非常常见,因此程序员掌握线程锁的使用是非常必要的。线程锁在C++中的<mutex>有非常全面的实现。这里面锁的类型具有代表性,分为条件锁、自旋锁等等,具体区别可以参考这篇博客。C++的多线程非常令人头大...这里就不展开了。 - 事件(Event):在多线程体系中,事件是一种常用于线程同步的机制,如果线程需要在运行过程中等待其他线程的运行,就可以使用事件机制。 ## C# C#中与线程相关的模块在System.Threading空间下。System.Threading.Thread提供了线程实现的类,使用delegate即可创建线程对象。这个空间底下也提供了SpinLockSemaphoreMutex等线程锁,以及AutoResetEvent实现了事件机制。System.Threading.ThreadPool则提供了线程池的实现。另外需要指出的是C#提供了lock关键字,只需对冲突的对象使用lock锁上,那么在其对应的上下文中就能够避免冲突。 ## Python Python中与线程相关的对象在threading模块中,其中Thread类提供了线程实现,Lock, Semaphore提供了线程锁,Event实现了事件机制。Python中可以使用with lock:这样的块实现与C#lock相似的语法,但是这个地方的lock仍然需要自己声明,不如C#和Java中的lock用着方便。

总体而言C#和Python对多线程机制的支持都比较全面,然而CPython有一个臭名昭著的全局锁GIL,使得其多线程效率大幅下降。因此在很多Python库中,大家宁愿使用multiprocess多进程来进行并行(即便需要处理进程间通信的问题),也不愿使用threading来完成并行任务。这一点上不得不说Python辣鸡!

协程

协程应该是21世纪才用的比较多的技术了,并且这个概念应该是在Go里面提的最多。在前文我提到协程是并行时打了引号,这是因为协程本质上还是同一个时刻只能干一件事,没法利用硬件并行,因此我们形容协程都是用“异步”(Asychronized)而不是“并行”(Parallel)。异步是与同步相对的,只要程序能一会干点这个,一会干点那个,不按顺序来,那就可以称作异步了。协程的广泛应用是由于近些年大型服务器的负载越来越大,并发需求越来越高(同时剁手的人越来越多),多任务切换的开销越来越不可忽视,因此协程这个开销最小的方法就被广泛应用了。协程实际上不是一个比线程更小的概念,而是另一类概念(并行/串行 vs 异步/同步)。协程的特点是一个任务能够跑到一半就暂停,然后把状态存起来,等到需要的东西备齐了以后再把状态复原接着跑;至于暂停之前和之后是不是在同一个线程上跑、有没有跟别的任务一块跑并不重要。因此实际上协程是回调(Callback)机制的一个封装升级。 ## 一些概念 - 事件循环(Event loop):事件循环是一种非常简单的实现异步的机制,简而言之就是维护一个队列,然后把队列里的任务挨个执行,而任务随时随地可以被添加进队列。 - 异步执行/等待(async/await):这两个关键词在多个语言中都有出现。async用来修饰函数,说明这个函数可以异步执行;await用来等待异步函数的结束,如果没有结束就把当前任务搁着。 ## C# C#中没有协程的概念,C#在5.0版本中引入的async/await关键字提供了异步执行的接口。据我所知C#应该是最早一批引入这个概念的语言了,并且C#里面async和await的使用非常顺滑~。C#的async/await调度与Go一样,都是通过线程池实现,因此性能也非常不错。C#中与async/await有关的接口在System.Threading.Tasks下,里面的Task类型是对能够await的对象的封装。

C#中也有用到Event loop来实现异步的地方,一般是在UI相关的函数中,例如整个C#里面的event机制都是通过事件循环来实现的。使用事件循环来完成与UI相关的异步应该是非常标准的做法了,例如Qt里面也有QEventLoop来实现UI的异步回调。与Event loop相关的是Dispatcher机制,Dispatcher可以将指定任务加进事件循环中执行,例如在WPF中可以用Window的Dispatcher在其他线程中将任务加进UI主线程。

另外需要指出的是C#还可以通过yield关键词实现异步,yield return可能是C#最早的异步机制了,不过功能有限,只能与IEnumerable合作使用。C#中有一些协程的库(如Unity里的)就是使用yield机制来实现的。具体怎么使用yield还请去学习C#的语法~ ## Python Python对异步的支持就来的比较晚了,直到PEP 492才正式加入了对async关键字的支持,放在了asyncio模块中。Python对这对关键词的实现又很辣鸡了,采用的是Event loop机制来实现(可能是因为多线程性能太差了吧= =)。最让人蛋疼是为了执行异步函数你还需要自己开event loop,如果你之前开过一个了,那你还需要把之前那个loop找回来,然后dispatch进去,这是何其难受!。。

Python中只要对象有__await____aiter__或者__aenter__就可以分别支持awaitasync forasync with的代码块。Python还设计了三个相关概念:Coroutine代表异步对象、Task代表异步执行计划、Future代表异步执行结果。。何必呢???像C#用一个Task代表全部不行吗?再配合event loop的接口,就产生了create_taskrun_coroutine_threadsaferun_until_completerun_in_executor等我总是搞不清区别的函数。。。我爱C#!


以上是我对C#和Python中异步机制的总结,我对各语言底层的了解并不深,如有错漏还请指点~

Treat me some coffee XD