马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
15. TPL Dataflow 流水线编程
本章 GitHub 堆栈:csharp-concurrency-cookbook ⭐
接待 Star 和 Fork!全部代码示例都可以在堆栈中找到并运行。
一、本章导读
📌 本文目标:从零构建生产级 Dataflow 流水线,把握完备的筹划思绪、核心原理和最佳实践。
我先问你一个题目:假如让你处理惩罚 10 万张图片(下载 → 压缩 → 水印 → 上传),你会怎么写?
许多人的第一反应是:- // 方案1:串行处理 —— 太慢了
- foreach (var url in imageUrls)
- {
- var data = await DownloadAsync(url);
- var compressed = await CompressAsync(data);
- var watermarked = await AddWatermarkAsync(compressed);
- await UploadAsync(watermarked);
- }
- // 方案2:全部并行 —— 炸了
- await Task.WhenAll(imageUrls.Select(async url =>
- {
- // ... 10万个Task同时跑,内存几十GB,系统直接崩溃
- }));
复制代码 方案 1 太慢,10 万张图片大概要跑几天。
方案 2 太猛,刹时把内存撑爆,体系直接 OOM。
那怎么办?你大概想到用 SemaphoreSlim 或 Channel 来限流:- var sem = new SemaphoreSlim(10); // 限制并发数
- await Task.WhenAll(imageUrls.Select(async url =>
- {
- await sem.WaitAsync();
- try
- {
- // ... 处理逻辑
- }
- finally
- {
- sem.Release();
- }
- }));
复制代码 这确实能跑,但有几个题目:
- 各阶段混在一起:下载、压缩、上传的并发度应该差别(I/O 麋集 vs CPU 麋集),但如今是同一控制
- 缺乏背压机制:假如下载很快但上传很慢,中心数据会堆积在内存里
- 错误处理惩罚复杂:某个文件失败了要记载日志
、重试、还要继续处理惩罚其他文件
- 代码难维护:加个新阶段(好比"人脸辨认"),代码要大改
二、Dataflow 的核心头脑
2.1 流水线的本质
你去过工厂吗?当代工厂的生产线是如许的:- [原料] → [工位1:切割] → [工位2:打磨] → [工位3:组装] → [工位4:包装] → [成品]
- ↓ 3人并行 ↓ 5人并行 ↓ 2人并行 ↓ 1人
复制代码 每个工位的特性:
- ✅ 独立负责一道工序:切割的人只管切割,不管打磨
- ✅ 有本身的工人数目(并行度):打磨工序慢,多配5个人
- ✅ 有中心缓冲区(队列):前道工序快了,产物在这里列队等候
- ✅ 出题目只影响本工位:组装工位停机,不影响切割继续生产
如许做的长处是什么?
对比维度串行处理惩罚流水线处理惩罚服从处理惩罚完一个产物才气开始下一个同时处理惩罚多个产物(差别阶段)并行度无法针对差别工序调解慢工序多配人,快工序少配人故障影响一个环节出题目,全线歇工只影响本工位,其他继续运行资源利用CPU和I/O常常空闲CPU和I/O同时工作TPL Dataflow 把这套头脑搬到代码里:
graph LR A[数据源] --> B[BlockA
并行度:3
缓冲:10个] B --> C[BlockB
并行度:5
缓冲:20个] C --> D[BlockC
并行度:2
缓冲:5个] D --> E[效果] style B fill:#1976d2,stroke:#0d47a1,stroke-width:2px,color:#fff style C fill:#388e3c,stroke:#1b5e20,stroke-width:2px,color:#fff style D fill:#f57c00,stroke:#e65100,stroke-width:2px,color:#fff每个 Block(块) 就是一个工位,它:
- 吸收上游数据(或自动拉取)→ 相称于从上一道工序拿产物
- 实行本身的逻辑(同步或异步)→ 相称于本工位的加工利用
- 把效果传给鄙俚(或抛弃)→ 相称于把成品放到下一道工序的缓冲区
2.2 核心Block范例
相干类在System.Threading.Tasks.Dataflow定名空间内里,体系自带,无须额外nuget包。
Block 范例功能典范场景ActionBlock斲丧数据,无输出日志 记载、数据库写入TransformBlock转换数据(1→1)图片压缩、数据洗濯TransformManyBlock转换数据(1→多)拆分大文件、睁开嵌套数据BatchBlock批量打包(多→1)批量入库、日志 聚合BroadcastBlock广播数据(1→多份)缓存+长期化双写BufferBlock缓冲队列解耦生产者和斲丧者2.3 关键设置参数
- var options = new ExecutionDataflowBlockOptions
- {
- MaxDegreeOfParallelism = 4, // 并行度(同时处理4条数据)
- BoundedCapacity = 10, // 缓冲容量(最多排队10条)
- CancellationToken = cts.Token // 取消令牌
- };
- var block = new TransformBlock<string, byte[]>(
- async url => await DownloadAsync(url),
- options
- );
复制代码 为什么 Dataflow 比手写流水线优雅?
手写流水线要本身做的事Dataflow 内置支持❌ 手动管理队列(Channel、Queue)✅ 每个块自带缓冲队列❌ 手动控制并行度(SemaphoreSlim)✅ MaxDegreeOfParallelism 设置❌ 手动实现背压(队列满了要等候)✅ BoundedCapacity 自动节流❌ 手动流传完成状态(上游竣事了关照鄙俚)✅ PropagateCompletion 自动流传❌ 手动处理惩罚非常(某个任务失败了要记载)✅ 每个块独立非常处理惩罚❌ 手动组装流水线(代码像意大利面条)✅ 声明式 LinkTo 毗连三、何时利用 Dataflow?
✅ 恰当的场景
场景为什么选 Dataflow多阶段数据处理惩罚每个阶段独立设置并行度和缓冲I/O 和 CPU 肴杂任务下载(I/O麋集)并行20,压缩(CPU麋集)并行4须要背压控制自动节流,制止内存爆炸复杂数据流向支持分支、归并、过滤、广播长时间运行的流水线可观测 性强(监控 、统计、故障隔离)❌ 不恰当的场景
场景为什么不选 Dataflow保举方案简单并行任务Dataflow 有学习本钱和性能开销Parallel.ForEachAsync及时变乱处理惩罚Dataflow 不是变乱驱动架构System.Reactive (Rx)须要严酷序次默认是并行的,手动包管序次很贫苦Channel 或串行处理惩罚对比其他方案
- // 方案1:Parallel.ForEachAsync —— 简单并行任务首选
- await Parallel.ForEachAsync(urls, new ParallelOptions { MaxDegreeOfParallelism = 10 },
- async (url, ct) => await ProcessAsync(url));
- // 方案2:Channel<T> —— 单生产者/单消费者场景
- var channel = Channel.CreateBounded<string>(10);
- var producer = Task.Run(async () => { /* ... */ });
- var consumer = Task.Run(async () => { /* ... */ });
- // 方案3:Dataflow —— 多阶段流水线
- var pipeline = CreatePipeline(); // 本文重点!
- await pipeline.SendAsync(data);
复制代码 四、实战:构建生产级图片处理惩罚流水线
完备代码位置:Dataflow/Production/ProductionImagePipeline.cs
4.1 架构总览
我们要实现一个真实生产环境可用的图片处理惩罚流水线:
graph TB A[输入: 图片URL] --> B[阶段1: 下载
I/O麋集, 并行20
支持重试] B --> C[阶段2: 压缩
CPU麋集, 并行4
背压控制] C --> D[阶段3: 水印
CPU麋集, 并行4] D --> E[阶段4: 上传
I/O麋集, 并行10
故障隔离] E --> F[阶段5: 日志
串行, 落盘] F --> G[完成] H[监控 线程] -.定期巡检.-> B H -.定期巡检.-> C H -.定期巡检.-> D H -.定期巡检.-> E style B fill:#1976d2,stroke:#0d47a1,stroke-width:2px,color:#fff style C fill:#388e3c,stroke:#1b5e20,stroke-width:2px,color:#fff style D fill:#f57c00,stroke:#e65100,stroke-width:2px,color:#fff style E fill:#d32f2f,stroke:#b71c1c,stroke-width:2px,color:#fff style F fill:#7b1fa2,stroke:#4a148c,stroke-width:2px,color:#fff style H fill:#455a64,stroke:#263238,stroke-width:2px,color:#fff筹划要点:
- 故障隔离:某张图片失败了,不影响其他图片继续处理惩罚
- 重试机制:下载/上传支持指数退避重试
- 监控
本领:及时统计乐成率、吞吐量、队列积蓄
- 优雅关闭:取消时等候当前任务完成,不丢数据
4.2 核心代码实现
⚠️ 生产级代码的三大要求:
- 设置外部化:ExecutionDataflowBlockOptions 应由调用方注入(从设置文件读取)
- 全链路取消:CancellationToken 必须通报到每个 Block
- 可观测
性:袒露 Block 的公共方法(InputCount、Completion 等)供监控利用
Step 0: 设置化筹划(生产环境必备)
- /// <summary>
- /// 流水线配置选项(由调用方通过配置文件注入)
- /// </summary>
- public class PipelineOptions
- {
- /// <summary>下载块配置(I/O 密集,默认高并行)</summary>
- public ExecutionDataflowBlockOptions? DownloadOptions { get; set; }
- /// <summary>压缩块配置(CPU 密集,默认=核心数)</summary>
- public ExecutionDataflowBlockOptions? CompressOptions { get; set; }
- /// <summary>上传块配置(I/O 密集,默认高并行)</summary>
- public ExecutionDataflowBlockOptions? UploadOptions { get; set; }
- /// <summary>HTTP 超时时间(默认30秒)</summary>
- public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(30);
- /// <summary>上传重试次数(默认3次)</summary>
- public int MaxUploadRetries { get; set; } = 3;
- /// <summary>监控报告间隔(默认5秒)</summary>
- public TimeSpan MonitorInterval { get; set; } = TimeSpan.FromSeconds(5);
- /// <summary>获取默认配置</summary>
- public static PipelineOptions Default => new()
- {
- DownloadOptions = new ExecutionDataflowBlockOptions
- {
- MaxDegreeOfParallelism = 10,
- BoundedCapacity = 20
- },
- // ... 其他配置
- };
- }
- // 构造函数支持配置注入
- public ProductionImagePipeline(
- ILogger<ProductionImagePipeline> logger,
- PipelineOptions? options = null) // ✅ 如果为 null 则使用默认配置
- {
- _options = options ?? PipelineOptions.Default;
- // ...
- }
复制代码 为什么要设置外部化?
硬编码设置外部化设置❌ 差别环境(开发/测试/生产)参数雷同✅ 根据环境调解(生产环境并行度更高)❌ 修改参数须要重新编译、发布✅ 修改设置文件即可,无需重新摆设❌ 无法动态调优性能✅ 可以根据监控数据调解设置生产环境利用示例:- // appsettings.json
- {
- "Pipeline": {
- "DownloadParallelism": 20,
- "CompressParallelism": 8,
- "HttpTimeoutSeconds": 60
- }
- }
- // Startup.cs
- var config = Configuration.GetSection("Pipeline");
- var options = new PipelineOptions
- {
- DownloadOptions = new ExecutionDataflowBlockOptions
- {
- MaxDegreeOfParallelism = config.GetValue<int>("DownloadParallelism"),
- BoundedCapacity = 50
- },
- HttpTimeout = TimeSpan.FromSeconds(config.GetValue<int>("HttpTimeoutSeconds"))
- };
- var pipeline = new ProductionImagePipeline(logger, options);
复制代码 Step 1: 界说数据结构
- // 流水线中流转的数据(携带状态,而不是抛异常)
- public record ProcessResult
- {
- public string ImageUrl { get; init; } = "";
- public byte[]? ImageData { get; set; }
- public byte[]? CompressedData { get; set; }
- public byte[]? WatermarkedData { get; set; }
- public string? UploadedUrl { get; set; }
-
- public bool IsSuccess { get; set; } = true;
- public string? ErrorMessage { get; set; }
- public int RetryCount { get; set; }
-
- public ProcessResult WithError(string error)
- {
- IsSuccess = false;
- ErrorMessage = error;
- return this;
- }
- }
复制代码 为什么用 ProcessResult 而不是抛非常?
方案长处缺点抛非常代码简便一个非常会让整个 Block 制止,影响其他数据返回效果对象故障隔离,失败的数据继续往下走,记载日志须要在每个阶段查抄 IsSuccessStep 2: 创建各阶段Block
下载Block(支持重试):- private TransformBlock<ProcessResult, ProcessResult> CreateDownloadBlock()
- {
- return new TransformBlock<ProcessResult, ProcessResult>(
- async result =>
- {
- if (!result.IsSuccess) return result; // 上游已失败,跳过
- for (int retry = 0; retry <= 3; retry++)
- {
- try
- {
- _logger.LogInformation("下载 {Url} (重试 {Retry})", result.ImageUrl, retry);
- result.ImageData = await DownloadImageAsync(result.ImageUrl);
- _stats.IncrementDownloaded();
- return result;
- }
- catch (Exception ex)
- {
- if (retry == 3)
- {
- _logger.LogError(ex, "下载失败 {Url}", result.ImageUrl);
- return result.WithError($"下载失败: {ex.Message}");
- }
- await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retry))); // 指数退避
- }
- }
- return result;
- },
- new ExecutionDataflowBlockOptions
- {
- MaxDegreeOfParallelism = 20, // I/O密集,高并发
- BoundedCapacity = 50, // 限制排队数量
- CancellationToken = _cts.Token
- }
- );
- }
复制代码 Step 3: 组装流水线
- private TransformBlock<ProcessResult, ProcessResult> CreateCompressBlock()
- {
- return new TransformBlock<ProcessResult, ProcessResult>(
- result =>
- {
- if (!result.IsSuccess) return result;
- try
- {
- result.CompressedData = CompressImage(result.ImageData!);
- _stats.IncrementCompressed();
- return result;
- }
- catch (Exception ex)
- {
- return result.WithError($"压缩失败: {ex.Message}");
- }
- },
- new ExecutionDataflowBlockOptions
- {
- MaxDegreeOfParallelism = 4, // CPU密集,不宜过高
- BoundedCapacity = 20,
- CancellationToken = _cts.Token
- }
- );
- }
复制代码 关键:PropagateCompletion- public class ProductionImagePipeline : IDisposable
- {
- private readonly TransformBlock<ProcessResult, ProcessResult> _downloadBlock;
- private readonly TransformBlock<ProcessResult, ProcessResult> _compressBlock;
- private readonly TransformBlock<ProcessResult, ProcessResult> _watermarkBlock;
- private readonly TransformBlock<ProcessResult, ProcessResult> _uploadBlock;
- private readonly ActionBlock<ProcessResult> _logBlock;
- public ProductionImagePipeline(ILogger<ProductionImagePipeline> logger)
- {
- _logger = logger;
-
- // 创建各个阶段
- _downloadBlock = CreateDownloadBlock();
- _compressBlock = CreateCompressBlock();
- _watermarkBlock = CreateWatermarkBlock();
- _uploadBlock = CreateUploadBlock();
- _logBlock = CreateLogBlock();
- // 连接流水线(重点!)
- var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
- _downloadBlock.LinkTo(_compressBlock, linkOptions);
- _compressBlock.LinkTo(_watermarkBlock, linkOptions);
- _watermarkBlock.LinkTo(_uploadBlock, linkOptions);
- _uploadBlock.LinkTo(_logBlock, linkOptions);
- }
- public async Task ProcessAsync(string imageUrl)
- {
- var result = new ProcessResult { ImageUrl = imageUrl };
- await _downloadBlock.SendAsync(result);
- }
- public async Task StopAsync()
- {
- _downloadBlock.Complete(); // 标记入口为"不再接收新数据"
- await _logBlock.Completion; // 等待最后一个阶段完成
- }
- }
复制代码 这行代码的作用:
sequenceDiagram participant A as DownloadBlock participant B as CompressBlock participant C as UploadBlock Note over A: Complete() 被调用 A->>A: 处理惩罚完剩余数据 A->>B: 自动调用 Complete() B->>B: 处理惩罚完剩余数据 B->>C: 自动调用 Complete() C->>C: 处理惩罚完剩余数据 Note over C: Completion 任务完成Step 4: 监控与统计
- var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
- _downloadBlock.LinkTo(_compressBlock, linkOptions);
复制代码 监控两个维度:
- 业务指标:乐成/失败数目
- 性能指标:各阶段队列积蓄(InputCount)
Step 5: CancellationToken 全链路通报(生产环境必备)
- private async Task MonitorProgressAsync()
- {
- using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
- while (await timer.WaitForNextTickAsync(_cts.Token))
- {
- _logger.LogInformation(
- "📊 统计: 下载 {Downloaded}, 压缩 {Compressed}, 上传 {Uploaded}, 失败 {Failed}",
- _stats.Downloaded, _stats.Compressed, _stats.Uploaded, _stats.Failed
- );
- _logger.LogInformation(
- "📈 队列积压: 下载 {DQ}, 压缩 {CQ}, 上传 {UQ}",
- _downloadBlock.InputCount, _compressBlock.InputCount, _uploadBlock.InputCount
- );
- }
- }
复制代码 为什么要全链路通报?
场景没有 CancellationToken有 CancellationToken用户按 Ctrl+C❌ Block 继续运行,无法制止✅ 立即制止,开释资源超时❌ 卡死在某个Block,整个流水线挂起✅ 自动取消,返回控制权康健查抄❌ 监控体系无法判断是否康健✅ 可以检测 IsCancellationRequested完备示例:- // ❌ 错误做法:没有传递 CancellationToken
- var options = new ExecutionDataflowBlockOptions
- {
- MaxDegreeOfParallelism = 10
- // 缺少 CancellationToken!
- };
- // ✅ 正确做法:合并内部和外部 CancellationToken
- var options = _options.DownloadOptions ?? PipelineOptions.Default.DownloadOptions!;
- options.CancellationToken = CancellationTokenSource
- .CreateLinkedTokenSource(_internalCts.Token, options.CancellationToken)
- .Token;
- // Block 内部也要支持取消
- await client.GetByteArrayAsync(url, options.CancellationToken);
复制代码 Step 6: Block 的告急公共方法(生产环境常用)
TransformBlock 和 ActionBlock 有一些非常告急的公共方法/属性,文档常常漏讲:
方法/属性功能典范场景InputCount获取输入队列长度监控背压、性能调优OutputCount获取输出队列长度诊断鄙俚斲丧慢的题目Completion获取完成状态 Task等候流水线竣事、监控故障Complete()标志为"不再吸收新数据"优雅关闭流水线Fault(Exception)手动触发故障状态严肃错误时欺压制止SendAsync(data)异步发送数据(支持背压)生产者推送数据Post(data)同步发送数据(队列满则抛弃)非关键数据推送实战示例 1:监控队列积蓄- // 外部调用方
- using var cts = new CancellationTokenSource();
- Console.CancelKeyPress += (s, e) =>
- {
- e.Cancel = true;
- cts.Cancel(); // 用户按 Ctrl+C
- };
- await pipeline.ProcessAsync(urls, cts.Token); // ✅ 传递进去
复制代码 实战示例 2:康健查抄- // 暴露公共方法供监控系统调用
- public (int Download, int Compress, int Upload) GetQueueLengths()
- {
- return (
- _downloadBlock.InputCount, // ✅ 如果持续增长,说明下载太快或压缩太慢
- _compressBlock.InputCount,
- _uploadBlock.InputCount
- );
- }
- // 监控任务
- while (true)
- {
- var queues = pipeline.GetQueueLengths();
- if (queues.Download > 100)
- {
- _logger.LogWarning("⚠️ 下载队列积压严重: {Count}", queues.Download);
- // 触发告警、限流等
- }
- await Task.Delay(5000);
- }
复制代码 实战示例 3:优雅关闭- // Kubernetes 或监控系统定期调用
- public bool IsHealthy()
- {
- // ✅ 检查是否进入故障状态
- return !_downloadBlock.Completion.IsFaulted &&
- !_compressBlock.Completion.IsFaulted &&
- _downloadBlock.InputCount < 200; // 队列积压不能太多
- }
- // 监控端点
- app.MapGet("/health", () =>
- {
- return pipeline.IsHealthy()
- ? Results.Ok("Healthy")
- : Results.StatusCode(503); // Service Unavailable
- });
复制代码 五、运行效果与利用示例
5.1 运行方式
- public async Task StopAsync(TimeSpan timeout)
- {
- _downloadBlock.Complete(); // ✅ 1. 停止接收新数据
- var completionTask = _logBlock.Completion; // ✅ 2. 获取完成Task
- var timeoutTask = Task.Delay(timeout);
- if (await Task.WhenAny(completionTask, timeoutTask) == timeoutTask)
- {
- _logger.LogWarning("超时,强制取消");
- _internalCts.Cancel(); // ✅ 3. 触发所有Block的CancellationToken
- }
- try
- {
- await completionTask; // ✅ 4. 等待真正结束
- }
- catch (OperationCanceledException)
- {
- // 正常取消
- }
- }
复制代码 输出示例(100张图片):- cd Dataflow
- dotnet run
- # 选择选项 5: 生产级流水线演示
复制代码 5.2 性能对比
- [12:00:05] 下载 https://example.com/image1.jpg (重试 0)
- [12:00:05] 下载 https://example.com/image2.jpg (重试 0)
- ...
- [12:00:10] 📊 统计: 下载 45, 压缩 38, 上传 30, 失败 2
- [12:00:10] 📈 队列积压: 下载 15, 压缩 7, 上传 8
- ...
- [12:00:35] ✅ 处理完成!成功 98 张,失败 2 张
复制代码 为什么 Dataflow 既快又省内存?
方案题目Dataflow 的解法串行下载完一张才处理惩罚下一张,CPU和网络都闲着流水线并行:下载图2的同时压缩图1全并行10万个Task同时跑,中心效果都在内存里背压控制:队列满了上游就等候5.3 其他Demo
项目中还包罗了根本Block的讲授示例:- // Demo: 对比串行/并行/Dataflow 三种方案
- // 位置:Dataflow/Production/ProductionPipelineDemo.cs -> RunComparisonDemo()
- // 结果(100张图片):
- // 串行处理: 125 秒
- // 全并行: 内存 2.8GB(崩溃风险)
- // Dataflow流水线: 18 秒,内存 150MB
复制代码 六、常见题目与最佳实践
Q1: 怎样设置公道的并行度?
履历公式:- # 选项 1: 基础Block演示(ActionBlock/TransformBlock/BufferBlock)
- # 选项 2: LinkTo路由演示(按条件分流数据)
- # 选项 3: 背压控制演示(BoundedCapacity效果)
- # 选项 4: 图片流水线简化版(不含重试和监控)
复制代码 Q2: BoundedCapacity 设置多大符合?
保举值:- // I/O 密集型(网络/磁盘):并发数 = 2 * CPU核心数 ~ 100
- MaxDegreeOfParallelism = 20;
- // CPU 密集型(图片压缩/加密):并发数 = CPU核心数
- MaxDegreeOfParallelism = Environment.ProcessorCount;
- // 混合型(既有I/O又有CPU):根据实测调整,通常 4~8
- MaxDegreeOfParallelism = 6;
复制代码 Q3: 非常处理惩罚的最佳实践?
Dataflow 的非常处理惩罚是生产环境中最轻易踩坑的地方。让我们对比三种方案:
方案对比
方案长处缺点实用场景Block内 try-catch简单直接某个Block非常会制止整个流水线快速原型返回 Result 对象 ✅故障隔离,失败数据继续流转代码稍复杂生产环境保举监控 Block.Completion捕捉未处理惩罚非常只能记载,无法规复兜底日志✅ 保举模式(本文接纳)
- // 上游很快、下游很慢的场景:设置较小的值(10-50)
- BoundedCapacity = 20; // 避免内存堆积
- // 上游不稳定(网络波动):设置较大的值(100-500)
- BoundedCapacity = 200; // 缓冲流量峰值
- // 实时性要求高:设置 1 或不设置
- BoundedCapacity = DataflowBlockOptions.Unbounded; // 立即处理
复制代码 ❌ 常见错误
- // 1. 每个Block内捕获异常,返回失败结果
- var block = new TransformBlock<ProcessResult, ProcessResult>(result =>
- {
- if (!result.IsSuccess) return result; // 短路:上游已失败,直接跳过
- try
- {
- // ... 业务逻辑
- return result;
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "处理失败: {Url}", result.ImageUrl);
- return result.WithError(ex.Message); // 继续往下游传递失败结果
- }
- });
- // 2. 最后一个阶段统一记录失败日志
- var logBlock = new ActionBlock<ProcessResult>(result =>
- {
- if (!result.IsSuccess)
- {
- _logger.LogError("❌ 最终失败: {Url}, 原因: {Error}",
- result.ImageUrl, result.ErrorMessage);
- _stats.IncrementFailed();
- }
- else
- {
- _logger.LogInformation("✅ 处理成功: {Url}", result.UploadedUrl);
- _stats.IncrementSuccess();
- }
- });
- // 3. 监控 Block.Completion(兜底)
- _ = Task.Run(async () =>
- {
- try
- {
- await _downloadBlock.Completion;
- }
- catch (Exception ex)
- {
- _logger.LogCritical(ex, "⚠️ 下载Block发生未捕获异常,流水线已中断!");
- // 触发告警、重启流水线等
- }
- });
复制代码 🎯 非常处理惩罚决议树
graph TD A[Block内发生非常] --> B{是否盼望继续处理惩罚
其他数据?} B -->|是| C[捕捉非常
返回失败Result] B -->|否| D[不捕捉
让Block.Completion非常] C --> E{是否须要重试?} E -->|是| F[在Block内实现
指数退避重试] E -->|否| G[直接返回
WithError效果] F --> H[末了一个阶段
同一记载日志] G --> H D --> I[监控Completion
触发告警/重启] style C fill:#388e3c,stroke:#1b5e20,stroke-width:2px,color:#fff style F fill:#1976d2,stroke:#0d47a1,stroke-width:2px,color:#fff style H fill:#7b1fa2,stroke:#4a148c,stroke-width:2px,color:#fff style D fill:#d32f2f,stroke:#b71c1c,stroke-width:2px,color:#fffQ4: 怎样实现条件路由(数据分流)?
- // 错误1: 直接抛异常,导致整个流水线停止
- var block = new TransformBlock<string, byte[]>(async url =>
- {
- return await DownloadAsync(url); // 如果抛异常,后续数据都无法处理!
- });
- // 错误2: 不检查上游失败状态,浪费计算资源
- var compressBlock = new TransformBlock<ProcessResult, ProcessResult>(result =>
- {
- // 缺少:if (!result.IsSuccess) return result;
- return CompressImage(result.ImageData); // 上游失败了,ImageData是null,这里会崩溃!
- });
- // 错误3: 吞掉异常不记录日志
- catch (Exception ex)
- {
- return result.WithError("失败"); // ❌ 丢失了异常详情!
- }
复制代码 Q6: Dataflow vs Channel?
特性DataflowChannel学习曲线陡峭(Block范例多)平缓(雷同队列)多阶段流水线✅ 原生支持❌ 须要手动串联多个Channel背压控制✅ BoundedCapacity✅ BoundedChannel并行度控制✅ MaxDegreeOfParallelism❌ 须要手动创建多个斲丧者性能稍慢(功能多)更快(轻量级)实用场景复杂流水线简单生产者-斲丧者选择发起:
- 1-2个阶段 → Channel
- 3+个阶段,且须要差别并行度 → Dataflow
七、小结
本章核心要点
- Dataflow 的本质:把"工厂流水线"搬到代码里,每个Block是一个工位
- 三大核心本领:
- 独立设置每个阶段的并行度和缓冲容量
- 自动背压控制,制止内存爆炸
- 声明式毗连,代码清楚易维护
- 生产级实践:
- 用 ProcessResult 携带状态,而不是抛非常
- 监控队列积蓄和业务指标
- 支持重试、优雅关闭、取消令牌
进阶学习
- 官方文档:TPL Dataflow (Task Parallel Library)
|