C#并发模型
一 并发含义
并发在程序的实现中,需要格外的下功夫,是开发可靠性和可拓展性程序的利器。
主流上对并发的理解是:不阻塞主线程/能够同时处理多件事情,这两种说法其实是相通的,但又不是很好理解。
不阻塞主线程,其实这里的主线程,是调用线程,也就是执行代码的线程,我们要将其理解为上下文。不阻塞的模式是有多种实现方法的,并发可算其中一种。
同时处理多件事情,这里指的也是上下文,其实也暗指不阻塞上下文,让其能够及时释放,处理其他事情,当然也可理解为多线程环境。
另外需要提出来的一点是,多线程算并发的一种实现,但是本文主要介绍并发的模式,模式是统一抽象的。
二 C#并发模型发展路线
- APM
.NET中的异步模型,一开始是将处理和完成分开的,就是Begin和End的实现,通过IAsyncResult进行数据传递,这增加了异步代码开发的复杂性,也降低了程序的可读性,所以应用起来是比较麻烦的,典型的API如下:
- EAP
EAP是基于委托和事件实现异步,也是将处理和完成分开,具体引用的方法就是BeginInvoke和EndInvoke,使用IAsyncResult进行数据传递,同样具有APM的问题。
- TPL
TPL是使用async/await关键字的引入,将处理和完成结合在一起,简化了程序的开发,极大的提升了开发效率。
其实并发模式并不会提升单个程序代码的执行速度,其是通过资源优化来提升效率的,何为资源优化,就需要了解CLR运行的环境。
CLR线程池中,是区分工作线程和I/O线程的,工作线程负责执行调度,I/O线程负责进行IO处理,所以并发资源优化就是在程序调度中(执行代码),发生IO的时候(大多数都是IO操作),将调度程序进行及时释放,而不是同步等待,让其能够处理其他程序,而IO程序进行处理IO,当IO完成后,IO线程通知调度程序(可能是UI或者是线程池等),接着处理下面程序,这样就释放了工作线程,也就是不阻塞工作线程,从而提升了执行效率。
并发模式就是如此设计的,下面主要以async/await来介绍上下文的运行,async/await的调度有如下几个原则:
- 第一个await及以上的程序都是调用线程执行的,不论在什么情况下
- 区分UI/Request线程的执行,当存在多个await的时候,第二个及以后的await代码块在UI/Request的情况下,不进行配置的话,都是UI/Request调度的,在线程池的情况下,则由线程池提供调度程序
那么在UI/Request线程的情况下,如何配置才能让第二个及之后的await代码块使用线程池调用呢,就是对Task的.ConfigureAwait(false)进行配置。
三 C#并发
1. 基于任务的异步模型
基于Task的async/await就是该异步模型
2. 并行
并行也是并发的一种模式,在不同的场景下,要有选择的使用并发的不同技术栈,这样才能最大化效率。密集I/0的情况下,async/await很适合,密集计算的情况下,并行则很适合。
- 数据并行 Parallel 和 PLINQ
数据并行是针对数据的一种并发模式,在C#中,就针对集合,有Parallel的For和ForEach,有PLINQ。
Parallel并行是.NET的封装,通过充分的利用多核的计算能力,提升效率,需要注意的是,这会导致无序,所以需要考虑清楚。
- 数据可以独立处理,无状态的场景
var source = Enumerable.Range(0, 10000); Parallel.ForEach(source, item => { Console.WriteLine($"输出:{item.ToString()}"); });
![]()
-
数据有状态,需要规约的场景(PLINQ可能更适合),这里需要注意处理结果的锁(处理具有幺半群的性质则不需要)
var source = Enumerable.Range(0, 100); Parallel.ForEach<int, long>(source, () => 0L, (item, state, acc) => acc + item, result => { Console.WriteLine($"运算结果为:{result.ToString()}"); // 注意共享状态 });
![]()
- PLINQ
PLINQ得益于LINQ的支持,可以在聚合的场景下有很好的作用,尤其是Aggregate对规约的支持。
var source = Enumerable.Range(0, 100); var result = source.AsParallel().Sum(); // 聚合 Console.WriteLine($"输出:{result.ToString()}");

PLINQ下的FP:Reduce规约
public static T Reduce<T>(this ParallelQuery<T> query, Func<T, T, T> func) { return query.Aggregate((acc, item) => func(acc, item)); } public static T Reduce<T>(this ParallelQuery<T> query, T seed, Func<T, T, T> func) { return query.Aggregate(seed, (acc, item) => func(acc, item)); } public static TSeed Reduce<TSeed, TMessage>(this ParallelQuery<TMessage> query, TSeed seed, Func<TSeed, TMessage, TSeed> func) { return query.Aggregate(seed, (acc, item) => func(acc, item)); }
- 任务并行 Parallel
任务并行是指同时执行多个独立的方法,这些方法应该是互不影响的。
Parallel.Invoke(() => { Console.WriteLine($"第一个方法"); }, () => { Console.WriteLine($"第二个方法"); }, () => { Console.WriteLine($"第三个方法"); });

- 异常处理
Parallel任务并行的时候,会将异常统一捕获AggregateException,(.NET中没有Supress的异常处理)。
try { Parallel.Invoke(() => { throw new TimeoutException("超时异常"); }, () => { throw new ArgumentException("参数异常"); }, () => { Console.WriteLine($"第三个方法"); }); } catch (AggregateException ex) { ex.Handle(exc => { Console.WriteLine($"异常类型:{exc.GetType()},异常内容:{exc.Message}"); return true; }); } catch (Exception ex) { }

3. 数据结构
- 不变数据结构
类库名称: System.Collections.Immutable

数据的不变性是解决并发过程中数据共享的完美方法,常用的不变数据结构包括:
ImmutableQueue -- 不可变队列
ImmutableStack -- 不可变栈
ImmutableList -- 不可变列表
ImmutableHashSet -- 不可变set
ImmutableDictionary -- 不可变字典
ImmutableDictionary -- 不可变排序字典
代码实例如下:
数据的不变性是解决并发过程中数据共享的完美方法,常用的不变数据结构包括:
ImmutableQueue -- 不可变队列
ImmutableStack -- 不可变栈
ImmutableList -- 不可变列表
ImmutableHashSet -- 不可变set
ImmutableDictionary -- 不可变字典
ImmutableDictionary -- 不可变排序字典
- 线程安全数据结构
类库名称: System.Collections.Concurrent
线程安全的数据结构主要应用CAS(Compare And Swap)算法,进行数据的类似乐观锁的同步操作。常用的线程安全数据结构如下:
ConcurrentDictionary -- 线程安全的字典
ConcurrentQueue -- 线程安全的队列
ConcurrentStack -- 线程安全的栈
ConcurrentBag -- 线程安全的背包
- 阻塞类数据结构
类库:System.Collections.Concurrent
阻塞类的数据结构是天然的生产者/消费者模式,提供区别于其他数据结构的操作,如:停止,这里总结的模式是:线程轮询的情况下,使用异常退出循环而不是标志位。
常用的阻塞类数据结构如下:
BlockingCollection -- 阻塞队列
BlockStack -- 阻塞栈
BlockBag -- 阻塞背包
4. 取消
取消是对系统可靠性的一种体现,在基于任务的开发过程中,取消的方法必须考虑,.NET中提供了CancellationTokenSource -> CancellationToken类进行取消,注意,该类对任务的取消是协助式的,也就是说,在轮询等语境中,必须手动添加取消的程序,这里需要注意的是,CancellationToken有两种取消的方法:
- 判断标志位 cts.Token.IsCancellationRequested;
- 抛出异常进行取消 cts.Token.ThrowIfCancellationRequested();引发 System.OperationCanceledException。
建议使用抛出异常的方式,很多类库采用的便是引发异常的做法来进行停止。
下面介绍下CancellationTokenSource 的其他几种方法:
- 注册回调
CancellationTokenSource 可以在取消触发(调用Cancel()方法)的时候,回调一部分程序Action,具体用法如下:
var cts = new CancellationTokenSource(); cts.Token.Register(() => { Console.WriteLine($"取消的时候进行回调"); });
2. 链式的取消标志
CancellationTokenSource 可以进行组合,当取消其时,将其他组合的CancellationTokenSource 也进行取消,具体用法如下:
var cts = new CancellationTokenSource(); var cts2 = new CancellationTokenSource(); var cts3 = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cts2.Token);
3.延时取消
CancellationTokenSource 可以配置进行延时取消,具体用法如下:
var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(2)); // 2s后取消
5. 异常处理
基于任务的异步模式,由于.NET的出色处理,可以直接用try catch进行异常捕获,但是有几种场景下,需要注意:
- 任务并行Paralle.Invoke
- Task.WhenAll(),Task.WhenAny(),Task.WaitAll(),Task.WaitAny();
上述情境中,由于执行体是多个任务或方法,所以异常是AggregateException。 在命令式的异常处理中,由于模板统一,所以会增加多的副作用,所以在FP中,鼓励使用Success和Fault的状态进行异常处理,而且这样做可以强制开发进行处理,将在下面的FP模块中介绍到Option和Result模式。Result模式就是Hsl使用的。
另外,Task.When 和 Task.Wait是有明显区别的,When在异步的语境中使用,Wait在同步的语境中使用,一定注意。
6. 锁和CAS(Atom原子处理)
本文涉及到异步模式,所以不过多的介绍线程同步的技术,可以参考《多线程》一文,这里将要介绍的是CAS的原子处理。
原子处理可以替代锁,类似于乐观锁的实现,主要应用于多线程共享数据的场景下,应用的就是.NET提供的Interlocked类。
场景一: 数字的原子操作
var res = 10; Interlocked.Increment(ref res); // 原子加1 Interlocked.Decrement(ref res); // 原子减1 Interlocked.Add(ref res, 10); // 任意加
场景二: 自定义原子操作类,用于多线程数据共享
由此可知,在多线程的数据共享中(当然,这样的情况要避免),可使用Interlocked进行原子处理,下面将自定义的AtomUtil代码示例:
public sealed class AtomUtil<T> where T : class { #region 构造函数 public AtomUtil(T value) { this.value = value; } #endregion 构造函数 #region 变量 private volatile T value; // 实例的当前值 public T Value => value; #endregion 变量 #region 方法 // 赋值方法 -- CAS算法 public T Swap(Func<T, T> factory) { T original, temp; do { original = value; temp = factory(original); } while (Interlocked.CompareExchange(ref value, temp, original) != original); return original; } #endregion 方法 }
四 模式总结
1. TDF
TDF是TPL DataFlow的简称,使用类库为:Microsoft.Tpl.Dataflow

TDF用来处理数据流,块可分为源块、转换块、目标块,源块就是负责生产数据,转换块用于数据处理,同FP中的Map/Bind类似,目标快用来处理数据。
TDF可以实现参与者模式,同F#的MailBoxProcesser类似的代理,每个块由消息缓存、状态、处理输出构成。
- 基础块
常用的块有BufferBlock、TransformBlock、ActionBlock。
BufferBlock负责缓存数据,可以起到节流的作用(通过配置DataflowBlockOptions的BoundedCapacity)。
TransformBlock用于数据的转换处理。
ActionBlock用于执行代码。
上述三种块的应用在代理(参与者模式)中体现。
块的连接执行和DDD中的CQRS是有相同优势的,可以帮助对业务流程进行梳理(DDD中的ES还可以实现事件溯源)。
- 块的取消
块的取消分为两个步骤:
- LinkTo设置PropagateCompletion = true,用于传递异常和取消
- Complete(),等待取消
实例代码如下:
// 1. 结束第一步,配置结束传递 var options = new DataflowLinkOptions() { PropagateCompletion = true // 传递异常和结束 }; // 2. 块组合 BufferBlock.LinkTo(TransfromBlock, options); TransfromBlock.LinkTo(ActionBlock, options); // 3. 结束 BufferBlock.Complete(); // 结束 await BufferBlock.Completion.ContinueWith(t => TransfromBlock.Complete()); await TransfromBlock.Completion.ContinueWith(t => ActionBlock.Complete()); await ActionBlock.Completion;
- 块的异常处理
- 代理
代理是参与者模式的实现,轻量级的生产消费,可以用于解耦业务处理。
代理可分为无状态代理(只处理),有状态处理(规约)和双向通信代理,具体实现如下:
public sealed class StatefulDataFlowAgent<TState, TMessage> : IAgent<TMessage> { #region 构造函数 public StatefulDataFlowAgent(TState initial, Func<TState, TMessage, Task<TState>> action, CancellationTokenSource Cts) { AccState = initial; var options = new ExecutionDataflowBlockOptions() { CancellationToken = Cts == null ? CancellationToken.None : Cts.Token, }; ActionBlock = new ActionBlock<TMessage>(async (data) => AccState = await action(AccState, data), options); } #endregion 构造函数 #region 变量 public TState AccState; // 累加状态 private ActionBlock<TMessage> ActionBlock; // 执行体 #endregion 变量 #region 方法 // 同步添加数据 public void Send(TMessage data) { ActionBlock.Post(data); } // 异步添加数据 public Task SendAsync(TMessage data) { return ActionBlock.SendAsync(data); } #endregion 方法 }
public sealed class StatefulReplayDataFlowAgent<TState, TMessage, TReplay> : IReplayAgent<TMessage, TReplay> { #region 构造函数 public StatefulReplayDataFlowAgent(TState initial, Func<TState, TMessage, Task<TState>> projection, Func<TState, TMessage, Task<Tuple<TState, TReplay>>> ask, CancellationTokenSource Cts) { AccState = initial; var options = new ExecutionDataflowBlockOptions() { CancellationToken = Cts == null ? CancellationToken.None : Cts.Token, }; ActionBlock = new ActionBlock<Tuple<TMessage, TaskCompletionSource<TReplay>>>( async (data) => { if (data.Item2 == null) AccState = await projection(AccState, data.Item1); else { var result = await ask(AccState, data.Item1); AccState = result.Item1; data.Item2.SetResult(result.Item2); } } , options ); } #endregion 构造函数 #region 变量 public TState AccState; // 累加状态 private ActionBlock<Tuple<TMessage, TaskCompletionSource<TReplay>>> ActionBlock; // 执行体 #endregion 变量 #region 方法 // 同步添加数据 public void Send(TMessage data) { ActionBlock.Post(Tuple.Create<TMessage, TaskCompletionSource<TReplay>>(data, null)); } // 异步添加数据 public Task SendAsync(TMessage data) { return ActionBlock.SendAsync(Tuple.Create<TMessage, TaskCompletionSource<TReplay>>(data, null)); } // 同步通讯 public TReplay Ask(TMessage data) { var cts = new TaskCompletionSource<TReplay>(); ActionBlock.Post(Tuple.Create(data, cts)); return cts.Task.Result; } // 异步通讯 public Task<TReplay> AskAsync(TMessage data) { var cts = new TaskCompletionSource<TReplay>(); ActionBlock.Post(Tuple.Create(data, cts)); return cts.Task; } #endregion 方法 }
public sealed class StatelessDataFlowAgent<T> : IAgent<T> { #region 构造函数 public StatelessDataFlowAgent(Func<T, Task> action, CancellationTokenSource Cts) { var options = new ExecutionDataflowBlockOptions() { CancellationToken = Cts == null ? CancellationToken.None : Cts.Token, }; ActionBlock = new ActionBlock<T>(async (data) => await action(data), options); } #endregion 构造函数 #region 变量 private ActionBlock<T> ActionBlock; #endregion 变量 #region 方法 // 同步添加数据 public void Send(T data) { ActionBlock.Post(data); } // 异步添加数据 public Task SendAsync(T data) { return ActionBlock.SendAsync(data); } #endregion 方法 }
2. Rx
Rx使用类库:System.Reactive

Rx是Reactive,主要用来处理连续的数据流,用推式的方法取代拉式,具有函数式的编程风格,目前总结下有两个方面可以应用。
- UI通知,通过节流实现UI的通知,防止UI线程假死
- 做观察者模式Subject
Rx使用.NET的IObservable和IObserver接口实现,IObservable是观察者,发布数据,IObserver是订阅者,订阅接收。
这里涉及到.NET中的IEnumrable、IQueryable、和IObservable(Linq to Events),有兴趣可以自己了解。
- Rx的转换
Rx可以和Event,TPL等进行转换。
// Rx注册 var changedObservable = Observable.FromEventPattern<FileSystemEventArgs>(FileWatcher, "Changed"); var createdObservable = Observable.FromEventPattern<FileSystemEventArgs>(FileWatcher, "Created"); var deletedObservable = Observable.FromEventPattern<FileSystemEventArgs>(FileWatcher, "Deleted");
- Rx处理
缓存、窗口、节流、超时、采样、....
Rx 的处理能力非常强大,其API非常丰富,需要不断琢磨System.Reactive.Linq。
1. 函数式编程FP
- 纯函数
纯函数是指输出仅有输入决定的函数,其没有副作用。
- 副作用
- 产生异常
- I/O
- 引用全局变量(状态)
- 异常处理
FP中,异常属于副作用,通过状态的传递来进行异常处理,也就是函数的处理有成功和失败,对其状态进行判断进行处理,Option包括成功和None,Result包括成功和失败,Result模式更加全面,具体实现如下:
public class Result<T> { #region 构造函数 // 正常情况 public Result(T ok) { this.Ok = ok; Error = default(Exception); } // 异常情况 public Result(Exception ex) { this.Error = ex; this.Ok = default(T); } #endregion 构造函数 #region 变量 public T Ok { get; } // 正常返回结果 public Exception Error { get; } // 异常返回结果 public bool IsFailed { get { return Error != null; } } // 结果是否为异常 public bool IsOk { get { return !IsFailed; } } // 结果是否为正常 #endregion 变量 #region 方法 // 结果处理 public R Match<R>(Func<T, R> okMap, Func<Exception, R> failureMap) { return IsOk ? okMap(Ok) : failureMap(Error); } // 结果处理 public void Match(Action<T> okMap, Action<Exception> failureMap) { if (IsOk) okMap?.Invoke(Ok); else failureMap?.Invoke(Error); } #region 隐式转换 // 正常结果 public static implicit operator Result<T>(T ok) => new Result<T>(ok); // 异常结果 public static implicit operator Result<T>(Exception ex) => new Result<T>(ex); #endregion 隐式转换 #endregion 方法 }
FP中也可以使用函数来进行异常处理,Catch的函数如下,其可以处理异常,并且不产生副作用:
public static Task<T> Catch<T, TError>(this Task<T> task, Func<TError, T> errorHandle) where TError : Exception { var tcs = new TaskCompletionSource<T>(); task.ContinueWith(innerTask => { if (innerTask.IsFaulted && innerTask?.Exception?.InnerException is TError) tcs.SetResult(errorHandle((TError)innerTask.Exception.InnerException)); else if (innerTask.IsCanceled) tcs.SetCanceled(); else if (innerTask.IsFaulted) tcs.SetException(innerTask?.Exception?.InnerException ?? new InvalidOperationException()); else tcs.SetResult(task.Result); }); return tcs.Task; }
- 函数组合
幺半群、单子、函子属于范畴论。
幺半群要求函数具有关联性和可交换性,且输入输出是同一种类型,如+、*,在并发下顺序不影响结果。
单子包含Return和Bind,Return用于提升,Bind用于转换,也就是函数进行组合。
函子用于解构函数,将多参数的函数解构为单个参数的函数,也用于函数组合。
- 分而治之(Fork/Join)
Fork/Join很类似与MapReduce,对数据进行转换、规约,可做节流、处理,用于IO密集型操作,代码如下:
using PL.Util.FP.Agent; using PL.Util.FP.Agent.Service; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; namespace PL.Util.FP { public static class ForkJoinUtil { /// <summary> /// Iemnurable拓展Fork/Join模式 /// </summary> /// <returns></returns> public static async Task<R> ForkJoin<T1, T2, R>(this IEnumerable<T1> source, Func<T1, Task<T2>> map, Func<R, T2, Task<R>> aggregate, R initianState, CancellationTokenSource cts, int paritionLevel = 8, int boundCapacity = 20) { cts = cts ?? new CancellationTokenSource(); // 数据缓存添加 var inputBuffer = new BufferBlock<T1>(new DataflowBlockOptions() { CancellationToken = cts.Token, BoundedCapacity = boundCapacity, }); // map转换 var blockOptions = new ExecutionDataflowBlockOptions() { CancellationToken = cts.Token, BoundedCapacity = boundCapacity, MaxDegreeOfParallelism = paritionLevel }; var mapperBlock = new TransformBlock<T1, T2>(map, blockOptions); // 组合 inputBuffer.LinkTo(mapperBlock, new DataflowLinkOptions() { PropagateCompletion = true, }); // reduce代理 var reduceAgent = AgentUtil.Start(initianState, aggregate, cts); // TDF转换为Rx,代理数据处理 IDisposable disposable = mapperBlock.AsObservable().Subscribe(async item => await reduceAgent.SendAsync(item)); // 数据添加 foreach (var item in source) { // Send允许延迟,Post会丢弃数据 await inputBuffer.SendAsync(item); } // 数据处理完成 inputBuffer.Complete(); var tcs = new TaskCompletionSource<R>(); await inputBuffer.Completion.ContinueWith(t => mapperBlock.Complete()); await mapperBlock.Completion.ContinueWith(t => { var agent = reduceAgent as StatefulDataFlowAgent<R, T2>; disposable.Dispose(); tcs.SetResult(agent.AccState); }); // 返回规约数据 return await tcs.Task; } } }
- 对象池
借助TDF实现异步池化的概念,代码如下:
public class ObjectPoolAsyncUtil<T> : IDisposable { #region 构造函数 public ObjectPoolAsyncUtil(int initialCount, Func<T> factory, CancellationToken cts, int timeOut = 0) { this.TimeOut = timeOut; this.BufferBlock = new BufferBlock<T>(new DataflowBlockOptions() { CancellationToken = cts }); this.Factory = () => factory(); for (int i = 0; i < initialCount; i++) this.BufferBlock.Post(this.Factory()); } #endregion 构造函数 #region 变量 private BufferBlock<T> BufferBlock; // 维持对象生命周期 private Func<T> Factory; // 获取对象方法 private int TimeOut; // 获取对象超时时间 #endregion 变量 #region 方法 #region 归还实例 public Task<bool> PutAsync(T t) { return this.BufferBlock.SendAsync(t); } #endregion 归还实例 #region 获取实例 public Task<T> GetAsync() { var tcs = new TaskCompletionSource<T>(); // CPS延续性风格,非阻塞的程序编写模式 BufferBlock.ReceiveAsync(TimeSpan.FromSeconds(TimeOut)).ContinueWith(t => { if (t.IsFaulted) { if (t.Exception.InnerException is TimeoutException) tcs.SetResult(this.Factory()); else tcs.SetException(t.Exception); } else if (t.IsCanceled) tcs.SetCanceled(); else tcs.SetResult(t.Result); }); return tcs.Task; } #endregion 获取实例 #region 销毁 public void Dispose() { this.BufferBlock.Complete(); } #endregion 销毁 #endregion 方法 }
- 延续性CPS
延续性实现完整的并发非阻塞模型。
利用ContinueWith和TaskCompletionSource。
1. 生产者消费者 Channel
使用类库:System.Threading.Channels

可以实现异步的有界队列和无界队列,具体代码可自己研究
五 问题处理
- UI/Requst情况下同步语义的禁用
在UI/Requset的情境下,应用Task的时候,禁止使用同步语义,如:
Task.Wait(),Task.WaitAll(),Task.Result等,这会导致假死。
2.async void的禁用
async void禁止使用,这会导致无法进行异常处理。



浙公网安备 33010602011771号