C# 并发编程学习笔记

术语含义

并发:同时做多件事情;

多线程:并发的一种形式,它采用多个线程来执行程序;

并行处理:把正在执行的大量任务分割成小块,分配给多个同时运行的线程;

异步编程:并发的一种形式, 采用future模式或回调(callback)机制,以避免产生不必要的线程。启动了的操作将会在一段时间后完成;

响应式编程:一种声明式的编程模式,程序在该模式中对事件作出响应;

使用new Thread表明项目代码太过时了;

 

并行的形式:

数据并行,指有大量的数据需要处理,并且每一块数据的处理过程基本上是彼此独立的;

任务并行,指需要执行大量任务,并且每个任务的执行过程基本上是彼此独立的;

 

数据并行的方法:

1.Parallel.Foreach,对资源更友好

2.PLINQ,values.AsParallel().Select(val=>IsPrime(val));

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

异步编程

  • 1.报告进度

使用IProgress<T>和Progress<T>类型
var progress = new Progress<double>(); progress.ProgressChanged += (sender,args) => { ... }; await MyMethodAsync(progress); 调用 if(progress!=null) progress.Report(...);

Progress<T>在创建时会捕获当前上下文,并且在这个上下文中调用回调函数。

所以如果UI线程中创建了Progress<T>,就能在回调函数中更新UI,即使异步方法是在后台线程中调用Report的。

  • 2.等待一组任务完成

正常await Task.WhenAll只捕获一个异常,如果需要捕获WhenAll中的所有异常,需要使用 AggregateException allExceptions = allTasks.Exception

  • 3.等待任意一个任务完成

Task.WhenAny返回的task对象永远不会以“故障”或“已取消”状态作为结束,如果这个任务完成时有异常,异常也不会传递给Task.WhenAny返回的Task对象。 因此,通常需要在Task对象完成后继续使用await;

  • 4.按顺序处理已完成的任务

static async Task<int> DelayAndReturnAsync(int val)
{
    await Task.Delay(TimeSpan.FromSeconds(val));
    return val;
}

static async Task ProcessTasksAsync()
{
    Task<int> taskA = DelayAndReturnAsync(2);
    Task<int> taskB = DelayAndReturnAsync(3);
    Task<int> taskC = DelayAndReturnAsync(1);
    var tasks = new[] { taskA, taskB, taskC };

    // 输出2,3,1
    //foreach(var task in tasks)
    //{
    //    var result = await task;
    //    Console.WriteLine(result);
    //}

    // 对序列求值,创建集合 = 开始执行任务
    // 输出1,2,3
    var processingTasks = tasks.Select(async t =>
    {
        var result = await t;
        Console.WriteLine(result);
    }).ToArray();

    await Task.WhenAll(processingTasks);

    // 使用NuGet包 Nito.Async
    //foreach (var task in tasks.OrderByCompletion())
    //{
    //    var result = await task;
    //    Console.WriteLine(result);
    //}
}
  • 5.避免上下文延续

如果大量async方法在UI上下文中恢复,可能会引起性能上的问题,如果没有必要恢复到原来的上下文,就用ConfigureAwait,如果一个async方法的一部分需要上下文,一部分不需要上下文,则可以考虑把它拆分为两个(或更多)async方法。微软公布了一个指导标准:每秒在UI线程中有100个左右的延续任务还尚可,每秒1000个就太多了。

  • 6.处理async Task方法异常

在async Task方法中引发的异常,存放在返回的Task对象中,只有当Task对象被await调用时,才会引发异常。

  • 7.处理async void方法异常

async void方法没有返回Task对象,无法存放异常,故无法在try..catch中捕获void方法中抛出的异常;

解决方法一:可以重载一个返回Task类型的重载,这样就能在await中捕获异常

解决方法二:使用NuGet包Nito.AsyncEx

try
{
    AsyncContext.Run(() => objectUnderTest.ExecuteWithoutTask("some argument"));
}
catch (Exception ex)
{
    exception = ex;
}

 

并行编程

用于分解计算密集型的任务片段,如果是I/O密集型任务请用异步编程方法。

Parallel类和并行LINQ只是为了使用方便,从而对Task类进行了封装。

  • 1.数据并行处理

ParallelLoopResult parallelLoopResult =
    Parallel.ForEach(
    matrices, //index为迭代次数,state是循环状态
        (matrix, state, index) =>
        {
            if (!matrix.IsInvertible)
            {
                state.Stop(); //执行Stop之后ParallelLoopResult.IsCompleted=false
            }
            else
            {
                matrix.Invert();
            }

return parallelLoopResult;

更常见的做法是取消并行循环(外部cancel),传入一个CancellationTokenSource

var cts = new CancellationTokenSource();

ParallelLoopResult parallelLoopResult = Parallel.ForEach( matrices, new ParallelOptions { CancellationToken = token }, (matrix, state, index) => {//....});

cts.Cancel();//如果并行循环在运行时会抛出异常                        

Parallel.For方法多用于多个数组的数据共用相同的索引的情况。

  • 2.并行聚合(累加和,平均值等)

Parallel类通过local value概念实现聚合,使用LocalFinally委托对每个局部值进行聚合,LocalFinally委托执行的线程跟Body是同一个线程。

Parallel.ForEach(
    source: values,
    localInit: () => 0,
    body: (item, state, localValue) => localValue + item,
    localFinally: (localValue) =>
    {
        // localFinally is invoke for each working task.
        lock (mutext)
        {
        // 以同步的方式对存放结果的变量进行访问
            result += localValue;
        }
    });

使用PLINQ对聚合的支持更有表现力,代码也更简洁

values.AsParallel().Sum();

// 使用累加器函数
values.AsParallel().Aggregate(seed: 0, func: (sum, item) => sum + item);
  •  3.并行调用

使用场景:需要并行调用一批方法,并且这些方法大部分都是相互独立的。

Parallel.Invoke(
                () => ProcessPartialArray(array, 0, array.Length / 2),
                () => ProcessPartialArray(array, array.Length / 2, array.Length));

对于简单的并行调用,Parallel.Invoke是不错的解决方案。如果要对每一个输入的数据调用一个操作(改用Foreach),或者每一个操作产生了一些输出(改用并行LINQ);

  • 4.动态并行

使用场景:并行任务的结构和数量要在运行时才能确定

private static void ProcessTree(Node root)
{
    var task = Task.Factory.StartNew(() => Traverse(root), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
    task.Wait();
}

private static void Traverse(Node current)
{
    Console.WriteLine(string.Format("Current Node:{0}\t Task:{1}\t Thread:{2}", current.Name, Task.CurrentId, Thread.CurrentThread.ManagedThreadId));
    //Thread.Sleep(500);
    if (current.Left != null)
    {
        //TaskCreationOptions.AttachedToParent将父任务与子任务同步。 默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。
        Task.Factory.StartNew(() => Traverse(current.Left), CancellationToken.None,
            TaskCreationOptions.AttachedToParent, TaskScheduler.Default);
    }
    if (current.Right != null)
    {
        Task.Factory.StartNew(() => Traverse(current.Right), CancellationToken.None,
            TaskCreationOptions.AttachedToParent, TaskScheduler.Default);
    }
}

如果任务没有“父/子”关系,那可以使用任务延续(continuation)的方法,安排任务一个接一个地运行。

在并发编程中,Task有两个作用:作为并行任务,或作为异步任务。

并行任务可以使用阻塞的成员函数,例如Task.Wait、Task.Result、Task.WaitAll等等,通常也是用AttachedToParent建立任务之间的“父/子”关系。并行任务的创建需要用Task.Run或者Task.Factory.StartNew。

相反,异步任务应该避免使用阻塞的成员函数,而应该使用await、Task.WhenAll和Task.WhenAny,异步任务不适用AttachedToParent,但可以通过await另一个任务,建立一种隐式的“父/子”关系。

  •  5.并行LINQ

使用场景:需要对一批数据进行并行处理,生成另外一批数据,或者对数据进行统计。

PLINQ为各种各样的操作提供了并行版本,包括where,select以及各种聚会运算,例如sum、average和更通用的aggregate。

 

数据流基础

TPL数据流可用来创建网格(mesh)和管道(pipleline),并通过它们以异步方式发送数据,这种“声明式编程”风格通常要先完整地定义网格,然后才能开始处理数据。

每个网格由各种相互链接的数据流块(block)构成,每个块只负责处理某个单独的步骤。

使用TPL数据流需要安装NuGet包:Microsoft.Tpl.Dataflow

BufferBlock:最基础的Block,提供FIFO处理

 

ActionBlock:顺序处理委托;

 

TransformBlock:充当数据转换处理的功能功能,内部维护了2个Queue,一个InputQueue,一个OutputQueue。InputQueue存储输入的数据,而通过Transform处理以后的数据则放在OutputQueue,通过Receive方法来阻塞的一个一个获取OutputQueue中的数据,只有OutputQueue中有数据时才会返回;

 

TransformManyBlock:一个输入数据可以对应多个输出数据;

 

BroadcastBlock:使所有与它链接的目标块都收到数据的副本,BroadcastBlock不保存数据,每一个数据发送到所有接收者之后,这条数据就会被后面最新的数据所覆盖,如果没有链接目标块,数据将会被丢弃,但BroadcastBlock总是会保存最后一个数据,如果有一个新的链接目标块,那么这个目标块就会收到这个数据;

 

 WriteOnceBlock:最最简单的Block,它只能储存一个数据,一旦这个数据被发送出去以后,这个数据还是留在Block中,并且不会被删除或被替换;

 

 BatchBlock:提供把多个单个的数据组合起来处理的功能,在数据量满足构造函数中定义的要求时,会处理放进OutputQueue中,当BatchBlock调用Complete告知Post数据结束的时候,会把InputQueue中剩余的数据打包放入OutputQueue中等待处理,而不管数量是否满足构造函数的要求。

BatchBlock两种执行模式:

//Greedy=false 非贪婪模式 需从N个Source接收一个数据后进行处理,Source指LinkTo连接到这个BatchBlock的Block,这种时候需要Source.Complete()并且等待Completion完成;
//Greedy=true 贪婪模式(默认) 从任何Post到batchBlock接收到目标个数后进行处理。

 

JoinBlock:等待一个数据组合,组合作为一个Tuple传递给目标Block,如果定义了JoinBlock<int, string>类型,那么JoinBlock内部会有两个ITargetBlock,一个接收int类型的数据,一个接收string类型的数据。只有两个Block都收到数据后,才会放到JoinBlock的OutputQueue中输出。

 

 BatchedJoinBlock:BacthBlock和JoinBlick的组合。JoinBlock是组合目标队列的一个数据,而BatchedJoinBlock是组合目标队列的N个数据,当然这个N可以在构造函数中配置。只要2个ITargetBlock中的数据个数加起来等于N就可以输出到OutputQueue。

  

  • 1.链接数据流块

var multiplyBlock = new TransformBlock<int, int>(item => item * 2);
var subtractBlock = new TransformBlock<int, int>(item => item - 2);
multiplyBlock.LinkTo(subtractBlock);
// 默认情况下,链接的数据流块只传递数据,不传递完成情况(或出错信息),要实现完成情况的传递,需要设置PropagateCompletion属性
multiplyBlock.LinkTo(subtractBlock, new DataflowLinkOptions { PropagateCompletion = true });

  // 向Dataflow发送信号,停止接收数据
  multiplyBlock.Complete();
  await multiplyBlock.Completion;

Completion属性只有在设置了Complete方法后才会有效

  • 2.处理数据流网格中发生的错误

// 如果数据流块内的委托抛出错误,这个块就进入故障状态,一旦数据流块进入故障状态,就会删除所有的数据(并停止接收新数据)
static async Task TransferException()
{
    try
    {
        var block = new TransformBlock<int, int>(item =>
        {
            if (item == 1)
                throw new InvalidOperationException("DataflowError");
            return item * 2;
        });
        block.Post(1);
        block.Post(2);
block.Complete();
// Completion属性返回一个任务,一旦数据流块执行完成,这个任务也完成,如果数据流块出错,这个任务也出错
await block.Completion;
    }
    catch (InvalidOperationException) { }
}

如果用PropagateCompletion这个参数传递完成情况,错误信息也会被传递,只不过被封装在AggregateException类中传递给下一个块。

数据流块收到传过来的出错信息后,即使它已经被封装在AggregateException中,仍会用AggregateException进行封装,经过多个链接后才被发现,这个原始错误就会被AggregateException封装很多层,这时候用AggregateException.Flatten可以简化错误处理过程。

  • 3.断开链接

链接建立后是无法修改过滤器的,必须先断开链接,然后用新的过滤器建立链接(DataflowLinkOptions.Append=false代表将链接前置)

IDisposable link = multiplyBlock.LinkTo(subtractBlock);
//....
link.Dispose();

multiplyBlock.LinkTo(subtractBlock, new DataflowLinkOptions { Append = false });
  • 4.限制流量

默认情况下,数据流块生成输出的数据后,会按照创建的次序逐个地尝试通过链接传递数据,每个数据流块维护一个输入缓冲区,在处理数据之前接收任意数量的数据。

有分叉是,一个源块链接了两个目标块,上面的做法就会导致第一个目标块不停地缓冲数据(目标块还来不及处理数据时就得对所有数据进行缓冲),第二个目标块永远没有机会得到数据。

解决方法是使用BoundedCapacity属性,来限制目标块的流量。

var sourceBlock = new BufferBlock<int>();
var options = new DataflowBlockOptions { BoundedCapacity = 1 };
var tagetBlockA = new BufferBlock<int>(options);
var tagetBlockB = new BufferBlock<int>(options);
sourceBlock.LinkTo(tagetBlockA);
sourceBlock.LinkTo(tagetBlockB);
  • 5.数据流块的并行处理

默认情况下每个数据流块是互相独立的,即使将它们链接起来后,它们也是独立运行的,因此每个数据流网格本身就有并行特性。

假如某个特定的数据流块的计算量特别大,那就可以设置MaxDegreeOfParallelism参数,使数据流块在处理输入的数据时采用并行的方式,默认值是1。

var multiplyBlock = new TransformBlock<int, int>(item => item * 2, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded });

难点:如何找出需要并行处理的数据流块?

解决方法:在调试时暂停数据流运行,在调试器中查看等待的数据线的数量(就是还没有被数据流块处理的数据项),如果等待的数据项很多就表明需要进行并行化处理。

  • 6.创建自定义的数据流块

希望一些可重用的程序逻辑在自定义数据流块中使用,可以通过使用Encapsulate方法,取出数据流网格中任何具有单一输入块和输出块的部分。

IPropagatorBlock<int, int> CreateMyCustomBlock()
{
    var multiplyBlock = new TransformBlock<int, int>(item => item * 2);
    var addBlcok = new TransformBlock<int, int>(item => item + 2);
    var divideBlock = new TransformBlock<int, int>(item => item / 2);
    var flowCompletion = new DataflowLinkOptions { PropagateCompletion = true };
    multiplyBlock.LinkTo(addBlcok, flowCompletion);
    addBlcok.LinkTo(divideBlock, flowCompletion);
    return DataflowBlock.Encapsulate(multiplyBlock, divideBlock);
}

 

posted @ 2023-03-15 08:45  Z大山  阅读(196)  评论(0编辑  收藏  举报