使用.Net TPL Dataflow 構建一個通用數據流處理管道

概述

任務並行庫(TPL TaskParallel Library)提供了數據流組件,以幫助提高啓用併發的應用程序的健壯性。 這些數據流組件統稱爲TPL數據流庫。該數據流模型通過爲粗粒度數據流和流水線任務提供進程內消息傳遞來促進基於參與者的編程。數據流組件基於TPL的類型和調度基礎結構,並與C#,Visual Basic和F#語言支持集成在一起,以進行異步編程。

https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/dataflow-task-parallel-library

一個具體的場景

在處理數據同步的過程中,我們一般需要經過以下一系列步驟:
1、從源數據庫讀取數據
2、處理和轉換化源數據
3、將處理後的數據保存到目標數據庫。

在對以上數據處理編程實現時,如果處理和轉換步驟比較多,並且邏輯上跨了領域,比較好的做法是抽象一個 Data Pipeline , 對每一步做一個Handler,然後在Pipe 中組裝 handler.

一個抽象管道的設計可能如下:

    public class CommonDataPipeline<Tin>
    {
      private List<IHandler> Handlers;
      private PipelineContext<Tin> context;
      public CommonDataPipeline()
      {
            this.Handlers = new List<IHandler>();
      }
      public CommonDataPipeline(object param)
            : this()
        {
            this.context = new PipelineContext<Tin>(this.Handlers, param);
        }
      public void AddHanlder(IHandler handler)
        {
            Handlers.Add(handler);
        }

    public virtual void Invoke()
        {
            context.InvokeNext();
        }

        public virtual async Task InvokeAsync()
        {
            context.InvokeNextAsync();
        }

     public Dictionary<string, object> Result
        {
            get
            {
                return this.context.OutputData;
            }
        }
    }

管道上下文數據:

    /// 管道上下文
    public class PipelineContext<Tin>
    {
    private readonly IEnumerable<IHandler> handlers;
    public IHandler CurrentHandler { get; set; }
    public Tin InputData { get; }
    public Dictionary<string, object> OutputData { get; set; }
    public PipelineContext(List<IHandler> handlers, object param)
        {
            this.handlers = handlers;
            this.InputData = (Tin)param;
            this.OutputData = new Dictionary<string, object>();
        }

   public void InvokeNext()
        {
            foreach (var hanlder in this.handlers)
            {
                CurrentHandler = hanlder;
                hanlder.Invoke(this);
            }
        }

   public void InvokeNextAsync()
        {
            foreach (var handler in this.handlers)
            {
                Task.Run(() => handler.Invoke(this));
            }
        }
    }

客戶端對管道的調用:


            //初始化管道.
            var pipeline = new CommonDataPipeline<TSource>(sourceData);

            // 數據抽取.
            pipeline.AddHanlder(new DataExtractHandler);

            // 數據裝載處理 .
            pipeline.AddHanlder(new  DataLoadHandler);

            // 發消息處理.
            pipeline.AddHanlder(new DataPublishHandler);
           
            pipeline.Invoke();

以上的實現,把處理邏輯分離了,抽象的管道上下文能力比較弱,並且處理器需要了解Context 的細節。

使用 TPL Dataflow Block 構建數據流處理管道

對一般數據處理過程進行一些分析,可以抽象成如下管道模型:

1、一個數據處理管道 是將一塊源數據 轉換或者裝配成 一塊目標數據

2、管道可以添加多種處理節點,上一個節點的輸出是下一個節點的輸入

按以上需求,可以定義出客戶端調用代碼(通過鏈式調用,提高可讀性 ):

              // 初始化管道
              // int 是輸入的數據類型
              // string 是輸出的數據類型
              var pipeline = new TestPipeline<int, string>();

              // StartHandler 表示有一個開始節點,傳入一個處理函數用Func<int,string> 表示
                 pipeline
                    .AddStartHandler(x =>
                    {
                        return $"{x}-first step-";
                    })
               // transfer 表示數據轉換處理,源數據是上一個節點的輸出,處理輸出的是和輸入的結構一樣,用於作爲下一個節點的輸入,所以處理函數表示爲 Func<string,string>
               // transfer 節點可以自由添加
                    .AddTransferHandler(src =>
                    {
                        src += " stre2-";
                        return src;
                    })
                    .AddTransferHandler(src =>
                    {
                        src += " stre3-";
                        return src;
                    })
               //  End 節點不需要輸出,只有輸入
                    .AddEndHandler(src =>
                    {
                        System.Console.WriteLine(src);

                    })
                    .Process(8).Wait();  

根據以上客戶端代碼,通用的管道處理模型使用 DataFlow 的 TransferBlock<> 實現如下:

public class CommonDataPipeline<TSource, TTarget>
    {
        private IPropagatorBlock<TSource, TTarget> _startBlock;

        private List<TransformBlock<TTarget, TTarget>> _tempBlock = new List<TransformBlock<TTarget, TTarget>>();
        
        public CommonDataPipeline<TSource, TTarget> AddStartHandler(Func<TSource, TTarget> func)
        {
            var actionBlock = new TransformBlock<TSource, TTarget>(func);
            _startBlock = actionBlock;
            return this;
        }

        public CommonDataPipeline<TSource, TTarget> AddTransferHandler(Func<TTarget, TTarget> func)
        {
            var transferBlock = new TransformBlock<TTarget, TTarget>(func);

            if (_tempBlock.Any())
            {
                _tempBlock.Last().LinkTo(transferBlock);
                _tempBlock.Add(transferBlock);

            }
            else
            {
                _tempBlock.Add(transferBlock);
                _startBlock.LinkTo(transferBlock);

            }

            return this;
        }

        public CommonDataPipeline<TSource, TTarget> AddEndHandler(Action<TTarget> action)
        {
            if (_tempBlock == null)
            {
                throw new ArgumentException(" need transfer node....");
            }

            var actionBlock = new ActionBlock<TTarget>(action);
            _tempBlock.Last().LinkTo(actionBlock);
            return this;
        }

        public void Process(TSource source)
        {
            _startBlock.Post(source);
        }

測試結果:

小結
TPL 的數據流塊還有很多類型的塊,如果我們有大量數據需要併發處理,可以使用BufferBlock 緩衝輸入流,構建 一個生產者--消費者模式的管道,然以將消費者並行,提高數據處理能力。

TransferBlock 和ActionBlock 是數據流塊基礎,可以基於此模型構建高可用高性能的數據處理管道。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章