Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >.Net Core中利用TPL(任务并行库)构建Pipeline处理Dataflow

.Net Core中利用TPL(任务并行库)构建Pipeline处理Dataflow

原创
作者头像
码农阿宇
发布于 2018-10-08 13:52:22
发布于 2018-10-08 13:52:22
1.7K00
代码可运行
举报
文章被收录于专栏:码农阿宇码农阿宇
运行总次数:0
代码可运行

在学习的过程中,看一些一线的技术文档很吃力,而且考虑到国内那些技术牛人英语都不差的,要向他们看齐,所以每天下班都在疯狂地背单词,博客有些日子没有更新了,见谅见谅

什么是TPL?

Task Parallel Library (TPL), 在.NET Framework 4微软推出TPL,并把TPL作为编写多线程和并行代码的首选方式,但是,在国内,到目前为止好像用的人并不多。(TPL)是System.ThreadingSystem.Threading.Tasks命名空间中的一组公共类型和API 。TPL的目的是通过简化向应用程序添加并行性和并发性的过程来提高开发人员的工作效率,TPL动态地扩展并发度,以最有效地使用所有可用的处理器。通过使用TPL,您可以最大限度地提高代码的性能,让我们专注于程序本身而不用去关注负责的多线程管理。

出自: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl

为什么使用TPL?

在上面介绍了什么是TPL,可能大家还是云里雾里,不知道TPL的好处到底是什么。

我在youtube上找到了一个优秀的视频,讲述的是TPL和Thread的区别,我觉得对比一下,TPL的优势很快就能体现出来,如果大家能打开的话建议大家一定要看看。

地址是:https://www.youtube.com/watch?v=No7QqSc5cl8

现如今,我们的电脑的CPU怎么也是2核以上,下面假设我的电脑是四核的,我们来做一个实验。

使用Thread

代码中,如果使用Thread来处理任务,如果不做特出的处理,只是thread.Start(),监测电脑的核心的使用情况是下面这样的。

每一条线代表CPU某个核心的使用情况,明显,随着代码Run起来,其实只有某一个核心的使用率迅速提升,其他核心并无明显波动,为什么会这样呢?

原来,默认情况下,操作系统并不会调用所有的核心来处理任务,即使我们使用多线程,其实也是在一个核心里面运行这些Thread,而且Thread之间涉及到线程同步等问题,其实,效率也不会明显提高。

使用TPL

在代码中,引入了TPL来处理相同的任务,再次监视各个核心的使用情况,效果就变得截然不同,如下。

可以看到各个核心的使用情况都同时有了明显的提高。

说明使用TPL后,不再是使用CPU的某个核心来处理任务了,而是TPL自动把任务分摊给每个核心来处理,处理效率可想而知,理论上会有明显提升的(为什么说理论上?和使用多线程一样,各个核心之间的同步管理也是要占用一定的效率的,所以对于并不复杂的任务,使用TPL可能适得其反)。

实验结果出自https://www.youtube.com/watch?v=No7QqSc5cl8

看了这个实验讲解,是不是理解了上面所说的这句。

TPL的目的是通过简化向应用程序添加并行性和并发性的过程来提高开发人员的工作效率,TPL动态地扩展并发度,以最有效地使用所有可用的处理器。

所以说,使用TPL 来处理多线程任务可以让你不必吧把精力放在如何提高多线程处理效率上,因为这一切,TPL 能自动地帮你完成。

TPL Dataflow?

TPL处理Dataflow是TPL强大功能中的一种,它提供一套完整的数据流组件,这些数据流组件统称为TPL Dataflow Library,那么,在什么场景下适合使用TPL Dataflow Library呢?

官方举的一个 栗子 再恰当不过:

例如,通过TPL Dataflow提供的功能来转换图像,执行光线校正或防红眼,可以创建管道数据流组件,管道中的每个功能可以并行执行,并且TPL能自动控制图像流在不同线程之间的同步,不再需要Thread 中的Lock。

TPL数据流库由Block组成,Block是缓冲和处理数据的单元,TPL定义了三种最基础的Block。

source blocksSystem.Threading.Tasks.Dataflow.ISourceBlock <TOutput>),源块充当数据源并且可以从中读取。

target blocksSystem.Threading.Tasks.Dataflow.ITargetBlock <TInput>,目标块充当数据接收器并可以写入。

propagator blocksSystem.Threading.Tasks.Dataflow.IPropagatorBlock <TInput,TOutput>),传播器块充当源块和目标块,并且可以被读取和写入。它继承自ISourceBlock <TOutput>ITargetBlock <TInput>

还有其他一些个性化的Block,但其实他们都是对这三种Block进行一些扩充,可以结合下面的代码来理解这三种Block.

Code Show

1.source block 和 target block 合并成propagator block.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private IPropagatorBlock<string, Dictionary<int, string>> Process1()
        {
            var bufferBlock = new BufferBlock<Dictionary<int, string>>();
            var actionBlock = new ActionBlock<string>(x =>
              {
                  Console.WriteLine($"Process1 处理中:{x}");
                  Thread.Sleep(5000);
                  var dic = new Dictionary<int, string> { { 0, x } };
                  dic.Add(1, "Process1");
                  bufferBlock.Post(dic);
              }, new ExecutionDataflowBlockOptions
              {
                  MaxDegreeOfParallelism = _maxDegreeOfParallelism
              });
            actionBlock.Completion.ContinueWith(_ =>
            {
                Console.WriteLine($"Process1 Complete,State{_.Status}");
                bufferBlock.Complete();
            });
            return DataflowBlock.Encapsulate(actionBlock, bufferBlock);
        }

可以看到,我定义了BufferBlock和ActionBlock,它们分别继承于ISourceBlock 和 ITargetBlock ,所以说,他们其实就是源块和目标块,在new actionBlock()中传入了一个Action<String>,该Action就是该Block所执行的任务。 最后,DataflowBlock.Encapsulate(actionBlock, bufferBlock)把源块和目标块合并成了一个传递块。

2.TransformBlock

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private IPropagatorBlock<Dictionary<int, string>, Dictionary<int, string>> Process2()
        {
            var block = new TransformBlock<Dictionary<int, string>, Dictionary<int, string>>(dic =>
                  {
                      Console.WriteLine($"Process2 处理中:{dic.First().Value}");
                      Thread.Sleep(5000);
                      dic.Add(2, "Process2");
                      return dic;
                  }, new ExecutionDataflowBlockOptions
                  {
                      MaxDegreeOfParallelism = _maxDegreeOfParallelism
                  }
               );

            block.Completion.ContinueWith(_ =>
            {
                Console.WriteLine($"Process2 Complete,State{_.Status}");
            });

            return block;
        }

TransfromBlock继承了IPropagatorBlock,所以它本身就是一个传递块,所以它除了要处理出入数据,还要返回数据,所以给new TransformBlock()中传入的是Func<TInput, TOutput>而不是Action<TInput>.

3.TargetBlock来收尾

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private ITargetBlock<Dictionary<int, string>> Process3()
        {
            var actionBlock = new ActionBlock<Dictionary<int, string>>(dic =>
               {
                   Console.WriteLine($"Process3 处理中:{dic.First().Value}");
                   Thread.Sleep(5000);
                   dic.Add(3, "Process3");
                   Console.WriteLine("Dic中的内容如下:");
                   foreach (var item in dic)
                   {
                       Console.Write($"{item.Key}:{item.Value}||");
                   }
                   Console.WriteLine();
               }, new ExecutionDataflowBlockOptions
               {
                   MaxDegreeOfParallelism = _maxDegreeOfParallelism
               });
            return actionBlock;
        }

TargetBlock只能写入并处理数据,不能读取,所以TargetBlock适合作为Pipeline的最后一个Block。

4.控制每个Block的并行度

在在构造TargetBlock(包括其子类)的时候,可以传入ExecutionDataflowBlockOptions参数,ExecutionDataflowBlockOptions对象里面有一个MaxDegreeOfParallelism属性,通过改制,可以控制该Block的同时处理任务的数量(可以理解成线程数)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
new ExecutionDataflowBlockOptions
               {
                   MaxDegreeOfParallelism = _maxDegreeOfParallelism
               }

5.构建Pipeline,连接Block

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public Task Builder()
        {
            _startBlock = Process1();
            var process2Block = Process2();
            var process3Block = Process3();

            _startBlock.LinkTo(process2Block, new DataflowLinkOptions() { PropagateCompletion = true });

            process2Block.LinkTo(process3Block, new DataflowLinkOptions() { PropagateCompletion = true });

            process3Block.Completion.ContinueWith(_ =>
            {
                Console.WriteLine($"Process3 Complete,State{_.Status}");
                Console.WriteLine("所有任务处理完成");
            });

            return process3Block.Completion;
        }

通过

ISourceBlock<TOutput>.LinkTo(ITargetBlock<TOutput> target, DataflowLinkOptions linkOption)

方法,可以把Block连接起来,即构建Pipeline,当DataflowLinkOptions对象的PropagateCompletion属性为true时,SorceBlock任务处理完成是,会把TargetBlock也标记为完成。

Block被标记为Complete 后,无法传入新的数据了,即不能再处理新的任务了。

6.Pipeline的运行

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void Process(string[] inputs)
        {
            if (inputs == null)
                return;
            foreach (var input in inputs)
            {
                _startBlock.Post(input);
            }
            _startBlock.Complete();
        }

Pipeline构建好后,我们只需要给第一个Block传入数据,该数据就会在管道内流动起来了,所有数据传入完成后,调用Block的Complete方法,把该Block标记为完成,就不可以再往里面Post数据了。

完整代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

namespace Tpl.Dataflow
{
    public class Pipeline
    {
        IPropagatorBlock<string, Dictionary<int, string>> _startBlock;
        private int _maxDegreeOfParallelism;

        public Pipeline(int maxDegreeOfParallelism)
        {
            _maxDegreeOfParallelism = maxDegreeOfParallelism;
        }

        public void Process(string[] inputs)
        {
            if (inputs == null)
                return;
            foreach (var input in inputs)
            {
                _startBlock.Post(input);
            }
            _startBlock.Complete();
        }

        public Task Builder()
        {
            _startBlock = Process1();
            var process2Block = Process2();
            var process3Block = Process3();

            _startBlock.LinkTo(process2Block, new DataflowLinkOptions() { PropagateCompletion = true });

            process2Block.LinkTo(process3Block, new DataflowLinkOptions() { PropagateCompletion = true });

            process3Block.Completion.ContinueWith(_ =>
            {
                Console.WriteLine($"Process3 Complete,State{_.Status}");
                Console.WriteLine("所有任务处理完成");
            });

            return process3Block.Completion;
        }

        private IPropagatorBlock<string, Dictionary<int, string>> Process1()
        {
            var bufferBlock = new BufferBlock<Dictionary<int, string>>();
            var actionBlock = new ActionBlock<string>(x =>
              {
                  Console.WriteLine($"Process1 处理中:{x}");
                  Thread.Sleep(5000);
                  var dic = new Dictionary<int, string> { { 0, x } };
                  dic.Add(1, "Process1");
                  bufferBlock.Post(dic);
              }, new ExecutionDataflowBlockOptions
              {
                  MaxDegreeOfParallelism = _maxDegreeOfParallelism
              });
            actionBlock.Completion.ContinueWith(_ =>
            {
                Console.WriteLine($"Process1 Complete,State{_.Status}");
                bufferBlock.Complete();
            });
            return DataflowBlock.Encapsulate(actionBlock, bufferBlock);
        }

        private IPropagatorBlock<Dictionary<int, string>, Dictionary<int, string>> Process2()
        {
            var block = new TransformBlock<Dictionary<int, string>, Dictionary<int, string>>(dic =>
                  {
                      Console.WriteLine($"Process2 处理中:{dic.First().Value}");
                      Thread.Sleep(5000);
                      dic.Add(2, "Process2");
                      return dic;
                  }, new ExecutionDataflowBlockOptions
                  {
                      MaxDegreeOfParallelism = _maxDegreeOfParallelism
                  }
               );

            block.Completion.ContinueWith(_ =>
            {
                Console.WriteLine($"Process2 Complete,State{_.Status}");
            });

            return block;
        }

        private ITargetBlock<Dictionary<int, string>> Process3()
        {
            var actionBlock = new ActionBlock<Dictionary<int, string>>(dic =>
               {
                   Console.WriteLine($"Process3 处理中:{dic.First().Value}");
                   Thread.Sleep(5000);
                   dic.Add(3, "Process3");
                   Console.WriteLine("Dic中的内容如下:");
                   foreach (var item in dic)
                   {
                       Console.Write($"{item.Key}:{item.Value}||");
                   }
                   Console.WriteLine();
               }, new ExecutionDataflowBlockOptions
               {
                   MaxDegreeOfParallelism = _maxDegreeOfParallelism
               });
            return actionBlock;
        }
    }
}

Main方法如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
static void Main(string[] args)
        {
            Console.WriteLine("请输入管道并发数:");
            if (int.TryParse(Console.ReadLine(), out int max))
            {
                var pipeline = new Pipeline(max);
                var task = pipeline.Builder();
                pipeline.Process(new[] { "码", "农", "阿", "宇" });
                task.Wait();
                Console.ReadKey();
            }
        }

测试运行如图:

我来解释一下,为什么是这么运行的,因为把管道的并行度设置为2,所以每个Block可以同时处理两个任务,所以,如果给管道传入四个字符 ,每个字符作为一个任务,假设传入  “码农阿宇”四个任务,会时这样的一个过程…..

  1. 码   农  两个首先进入Process1,
  2. 处理完成后,码  农   两个任务流出,
  3. Process1位置空出来, 阿  宇 两个任务流入 Process1,
  4. 码  农 两个任务流向 Process2,
  5. 阿  宇 从 Process1 处理完成后流出,此时Process1任务完成
  6. 码  农 流出 Process2 ,同时 阿 宇  流入 Process2 ……
  7. 依此类推….

该项目Github地址: https://github.com/liuzhenyulive/Tpl-Dataflow-Demo

参考文献:https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/dataflow-task-parallel-library

码字不易,如果对您有用,欢迎推荐和关注,谢谢

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
C# Dataflow
随着多核处理器的普及,如何高效地进行并发与并行编程,已经成为 C# 开发者不可回避的话题。虽然 .NET 提供了诸如 Task、Parallel 等多种并发工具,但在处理流水线(Pipeline)、异步消息传递等场景时,很多开发者会发现这些工具还不够“顺手”。此时,Dataflow 就派上了用场。
JusterZhu
2025/06/08
880
C# Dataflow
C# Dataflow
随着多核处理器的普及,如何高效地进行并发与并行编程,已经成为 C# 开发者不可回避的话题。虽然 .NET 提供了诸如 Task、Parallel 等多种并发工具,但在处理流水线(Pipeline)、异步消息传递等场景时,很多开发者会发现这些工具还不够“顺手”。此时,Dataflow 就派上了用场。
郑子铭
2025/07/02
700
C# Dataflow
TPL Dataflow组件应对高并发,低延迟要求
2C互联网业务增长,单机多核的共享内存模式带来的排障问题、编程困难;随着多核时代和分布式系统的到来,共享模型已经不太适合并发编程,因此actor-based模型又重新受到了人们的重视。
有态度的马甲
2020/04/16
3K0
TPL Dataflow组件应对高并发,低延迟要求
共享内存 & Actor并发模型到底哪个快?
共享内存利用多核CPU的优势,使用强一致的锁机制控制并发, 各种锁交织,稍不注意可能出现死锁,更适合熟手。
有态度的马甲
2021/08/05
7400
三分钟掌握共享内存 & Actor并发模型
多核时代,可并行多个线程,但是受限于资源对象,线程之间存在对共享内存的抢占/等待,实质是多线程调用对象的行为方法,这涉及#线程安全#线程同步#。
有态度的马甲
2021/08/05
7950
C#常见的四种经典查找算法
在编程领域,数据结构与算法是构建高效、可靠和可扩展软件系统的基石。它们对于提升程序性能、优化资源利用以及解决复杂问题具有至关重要的作用。今天大姚给大家分享四种C#中常见的经典查找算法。
追逐时光者
2024/10/25
1460
C#常见的四种经典查找算法
编程语言.NET 进程内队列 Channel 的入门与应用
最近,博主为 FakeRPC[1] 增加了 WebSocket[2] 协议的支持。这意味着,我们可以借助其全双工通信的特性,在一个连接请求内发送多条数据。FakeRPC 目前最大的遗憾是,建立在 HTTP 协议上而不是 TCP/IP 协议上。因此,考虑 WebSocket 协议,更多的是为了验证 JSON-RPC[3] 的可行性,以及为接下来的要支持的 TCP/IP 协议铺路。也许,你从未意识到这些概念间千丝万缕的联系,可如果我们把每一次 RPC 调用都理解为一组消息,你是不是就能更加深刻地理解 RPC 这个稍显古老的事物了呢?在编写 FakeRPC 的过程中,我使用了 .NET 中的全新数据结构 Channel 来实现消息的转发。以服务端为例,每一个 RPC 请求经过 CallInvoker 处理以后,作为 RPC 响应的结果其实并不是立即发回给客户端,而是通过一个后台线程从 Channel 取出消息再发回客户端。 那么,博主为什么要舍近求远呢?我希望,这篇文章可以告诉你答案。
郑子铭
2024/01/03
4550
编程语言.NET 进程内队列 Channel 的入门与应用
C# Hashtable和Dictionary区别
Hashtable和Dictionary都是.Net下的表示键值对的集合,那么我们在使用中该选择Hashtable还是Dictionary?下边我们看看他们之间的区别:
用户9127601
2021/11/01
1.3K0
C# BufferBlock
BufferBlock是C#中的一个数据流块(Dataflow Block),它提供了一个有界或无界的缓冲区,用于存储数据。类似于BlockingCollection,你可以使用Post方法往BufferBlock中添加数据,也可以通过Receive方法阻塞或异步地读取数据。在高性能方面,BufferBlock是C#中一种常用的选择。
JusterZhu
2023/10/24
4140
C#  BufferBlock
【愚公系列】2023年02月 .NET CORE工具案例-Workflow-Core轻量级工作流引擎(数据流转)
流程管理起源于生产组织和办公自动化领域,是针对日常工作中具有固定程序的活动提出的一个概念。目的是通过将工作分解成定义良好的任务、角色,按照一定的规则和过程来执行这些任务并对它们进行监控,达到提高办事效率、降低生产成本、提高企业生产经营管理水平和企业竞争力的目标。
愚公搬代码
2023/03/16
9490
【愚公系列】2023年02月 .NET CORE工具案例-Workflow-Core轻量级工作流引擎(数据流转)
并发集合与任务并行库:C#中的高效编程实践
在现代软件开发中,多核处理器已经成为标准配置,这为开发者提供了利用多线程编程来提升应用程序性能的机会。然而,传统的同步编程模型在面对高并发场景时显得力不从心,容易导致死锁、竞争条件等问题。为了简化并发编程,并提高程序的可维护性和可扩展性,.NET Framework引入了任务并行库(TPL,Task Parallel Library)和并发集合类型,这些工具使得编写高性能的并行代码变得更加简单。
Jimaks
2024/09/20
3120
【数据传输】进程内业务拆分的数据传输,可用于发布订阅或者传递通知。
          又是两个月没有写博客了,也有一个月没有玩单片机做手工学习了;前几天在某个群里看到,有个群友说自己用了个内存队列用来保存某个task的数据,然后在某一处又使用死循环来判断内存队列的数据是否大于0,针对这个问题,才引发了这一边博客,哈哈,之前看到过有些人碰到这种场景是开线程使用死循环来进行数据传输处理。其实针对这个问题,while并不算是一个很好的解决方案,具体的还得结合场景去进行判断如何找到最优的解决方案,在本篇博客,我会罗列出我所已知和这个议题相关的几种方案,以及写了的部分代码。
陈显达
2022/11/07
5340
KeyValuePair和Dictionary详解:「建议收藏」
1、KeyValuePair a、KeyValuePair 是一个结构体(struct); b、KeyValuePair 只包含一个Key、Value的键值对。 2、Dictionary a、Dictionary 可以简单的看作是KeyValuePair 的集合; b、Dictionary 可以包含多个Key、Value的键值对。
全栈程序员站长
2022/09/14
1.2K0
强大的RabbitMQ优先级队列让你轻松面对现实业务场景
说到队列的话,大家一定不会陌生,但是扯到优先级队列的话,还是有一部分同学是不清楚的,可能是不知道怎么去实现吧,其实呢,这东西已经烂大街了。很简单,用“堆”去实现的,在我们系统中有一个订单催付的场景,我们客户的客户在tmall,taobao下的订单,taobao会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像施华蔻,百雀林这样大商家一年起码能够给我们贡献几百万,所以理应当然,他们的订单必须得到优先处理,而曾今我们的后端系统是使用redis来存放的定时轮询,大家都知道redis只能用List做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用rabbitmq进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级,好了,废话不多说,我们来看看如何去设置。
星哥玩云
2022/07/08
3950
强大的RabbitMQ优先级队列让你轻松面对现实业务场景
.Net Core配置Configuration源码研究
最近又研究了一下.NetCore配置选项的源码实现,又学习到了不少东西。这篇文章先写一下IConfiguration的学习成果,Options的后面补上
蓝夏
2021/09/09
6820
三分钟总览微软任务并行库TPL
俗话说,不想开飞机的程序员不是一名好爸爸;作为微软技术栈的老鸟,一直将代码整洁之道奉为经典, 优秀的程序员将优雅、高性能的代码看成自己的脸面。
有态度的马甲
2021/08/05
5740
C#异步编程的四种实现方式
在现代软件开发中,异步编程已经成为提高应用程序性能和响应性的关键技术。C#作为.NET平台的核心语言,提供了多种异步编程模型来帮助开发者编写高效且易于维护的代码。本文将深入探讨C#中的四种主要异步实现方式:基于async和await的异步方法、基于Task的异步编程、基于IAsyncEnumerable的异步数据流以及基于TPL Dataflow的异步数据流处理。
Michel_Rolle
2024/10/11
3.1K0
.Net多线程编程—误用点分析
1 共享变量问题 错误写法: 所有的任务可能会共享同一个变量,所以输出结果可能会一样。 1 public static void Error() 2 { 3 for(int i=0;i<10;i++) 4 { 5 Task.Run(() => { Console.WriteLine("{0}", i); }); 6 } 7 } 正确写法: 将变量i赋给局部变量temp,使得每一个任务使用不同的i值。 1
甜橙很酸
2018/03/08
9280
.Net多线程编程—误用点分析
C#-笔记-集合数组全集-第三章
如果把一个对象放进去,结果打印的是命名空间 结论:我们将一个对象输出到控制台 默认情况下打印的就是这个对象所在的类的命名空间。
用户9857551
2022/06/28
6370
C#-笔记-集合数组全集-第三章
浅谈.Net异步编程的前世今生----异步函数篇(完结)
上一篇我们着重讲解了TPL任务并行库,可以看出TPL已经很符合现代API的特性:简洁易用。但它的不足之处在于,使用者难以理解程序的实际执行顺序。
独立观察员
2022/12/06
8040
浅谈.Net异步编程的前世今生----异步函数篇(完结)
相关推荐
C# Dataflow
更多 >
交个朋友
加入架构与运维学习入门群
系统架构设计入门 运维体系构建指南
加入[后端] 腾讯云技术交流站
后端架构设计 高可用系统实现
加入架构与运维工作实战群
高并发系统设计 运维自动化实践
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验