【憩园】C#并发编程之概述

时间:2021-01-06 05:17:21

写在前面

并发编程一直都存在,只不过过去的很长时间里,比较难以实现,随着互联网的发展,人口红利的释放,更加友好的支持并发编程已经成了主流编程语言的标配,而对于软件开发人员来说,没有玩过并发编程都会有点不好意思。本系列文章将会以C#语言为主,详细介绍并发编程。

什么是并发编程,其实很简单,并发编程就是在一台处理器上同时做多件事情,并发编程的目标就是充分利用处理器的每一个核,以达到最高的处理性能。举个例子,服务器在响应第一个请求的同时响应第二个请求。

并发编程的方向

多线程

线程是一个独立的运行单元,是操作系统中能够进行运算调度的最小单位,它包含于进程之中,是进程中的实际运行单位。每个线程都有自己独立的栈,但是与进程内的其他线程共享内存。现在的.NET程序都维护了一个线程池,里面有着一定数量的工作线程,这些线程等待着执行分配下来的任务,线程池也可以随时监测线程的数量,以备开发者根据业务情况灵活处理。多线程也是我们并发编程的技术基础。

并行编程

并行编程主要用于分解计算密集型的任务片段,并将其分配给多个线程。前提是,程序中的任务可以分割成多个相互独立的任务块,关键字是相互独立,如果依赖太大,就不适合用并行编程。

并行编程利用CPU的空闲资源,充分提高了CPU的利用率,提高了系统的吞吐量。在大多数情况下,服务器本身就已经具备了并行处理能力,当通过编程进行并行处理的时候,需要慎重,因为使用不当将会导致内存溢出等风险,同时也会因为占用服务器资源而导致服务器本身的并行处理能力显著下降,严重的时候回导致系统无法使用。所以在进行编程的时候,尽量不要处理过长或者过短的任务。

并行处理分为数据并行和任务并行,其实他们都使用到了动态调整的分割算法,在任务分割后分配给工作线程。可以通过以下两种方式实现并行编程,一种是Parallel.ForEach以及更加优美的PLINQ,这是并行编程的推荐处理方式,并且它们自带自动分配任务的算法,可以在运行时进行调整;

在编写并行任务的时候,需要注意的是闭包所带来的风险。因为闭包捕获的是引用而不是值,所以可以在不经意间共享这些变量。一个比较好的处理就是,在使用闭包外的变量的时候,可以在闭包内定义局部变量,用以规避闭包带来的变量共享问题。

需要说明的是,线程池会根据需要增加线程数量,线程池采用的是工作窃取队列,以尽可能的达到高效

异步编程

目前最常用的异步编程模型是TAB编程(基于任务的编程模式)。异步编程提高了响应能力,也实现了可扩展性。比较直观的是,大家在处理Winform的时候遇到过界面卡死的情况异步编程可以在程序运行的过程中继续相应用户的输入,而不会导致界面卡死,并提高了提高服务器端应用的TPS(Transactions Per Second)和 QPS (Queries Per Second)。

.NET4.5以后为异步编程引入了async和await关键字,async关键字加在方法声明上,主要用来配合方法内的await关键字,这两个关键字的引入,使得C#在异步编程上更加优雅。如下所示

   1:  public async Task DelayAsync()
   2:  {
   3:      await Task.Delay(1000);
   4:  }

异步编程的执行流程一般是,当系统运行至await,会暂停,并可以捕捉到当前的上线文,SynchronizationContext,如果该上线文为空,就会使用当前的TaskScheduler,该方法也会在这个上线文中继续执行。代码执行完以后,会尝试在原始的上下文中恢复运行。

注意:运行winform和asp.net请求时会采用UI上下文或者asp.net上下文,其他情况下则采用线程池上下文。

异步方法的等待方式有await,Task.Wait和Task<T>.Result。但是要避免是用Task.Wait和Task<T>.Result,因为他们在UI线程或者ASP.NET线程环境中会导致死锁。这个地方需要说明一下死锁问题

   1:  public async Task DelayAsync()
   2:  {
   3:      await Task.Delay(1000);//捕捉当前上下文,并试图在已捕捉的上下文中继续运行
   4:  }
   5:   
   6:  void Test()
   7:  {
   8:      Task task= DelayAsync();
   9:      Task.Wait();//同步程序块,正在等待异步方法完成=======阻塞线程
  10:  }

UI或者asp.net的上下文每次只能同时运行一个线程。Wait方法已经阻塞了一个线程,所以在await的时候无法捕捉上下文。可以使用ConfigureAwait方法,设置参数continueOnCapturedContext为false。由此,可以带来一个启示,就是在线程池线程上使用ConfigureAwait(false),在用户界面或接口代码中再恢复过来。

异步编程中有一条重要的准则就是,当你使用了异步编程的时候,最好一直使用,也是为了防止死锁。

优化使用:

避免上下文延续,延续任务过多会导致性能问题

如果一个async方法一个需要用到上下文一个不需要用到,可以考虑拆分为两个async方法,这样代码组织也会更直观。

写到最后

以上只是提出了C#并发编程的引子,后面将会详细介绍C#并发编程的知识点。当然,C#并发编程还有其他内容,比如响应式编程和TPL数据流这些,我平时用的比较少,所以此处没有再做介绍,有兴趣的同学可以另外查看一下。