【问题标题】:How to reset a postponed / declined message in TPL Dataflow如何在 TPL 数据流中重置延迟/拒绝消息
【发布时间】:2019-04-24 10:27:19
【问题描述】:

我在我的应用程序中使用 TDF,到目前为止效果很好,不幸的是,我偶然发现了一个特定问题,它似乎无法直接用现有的 Dataflow 机制处理:

我有 N 个生产者(在本例中为 BufferBlocks),它们都只链接到 1 个(都指向同一个)ActionBlock。这个块总是一次处理 1 个项目,并且也只有 1 个项目的容量。

对于从生产者到ActionBlock的链接我也想加一个过滤器,但是这里的特殊情况是过滤条件可以独立于被处理的物品而改变,并且物品不能被丢弃! 所以基本上我想处理所有项目,但是当一个项目被处理时,订单/时间可能会改变。

不幸的是,我了解到,如果一个项目被“拒绝”一次 -> 过滤条件评估为 false,并且如果该项目没有传递给另一个块(例如 NullTarget),则目标块不会重试相同的项目(并且不会重新评估过滤器)。

public class ConsumeTest
  {
    private readonly BufferBlock<int> m_bufferBlock1;
    private readonly BufferBlock<int> m_bufferBlock2;
    private readonly ActionBlock<int> m_actionBlock;

    public ConsumeTest()
    {
      m_bufferBlock1 = new BufferBlock<int>();
      m_bufferBlock2 = new BufferBlock<int>();

      var options = new ExecutionDataflowBlockOptions() { BoundedCapacity = 1, MaxDegreeOfParallelism = 1 };
      m_actionBlock = new ActionBlock<int>((item) => BlockAction(item), options);

      var start = DateTime.Now;
      var elapsed = TimeSpan.FromMinutes(1);

      m_bufferBlock1.LinkTo(m_actionBlock, x => IsTimeElapsed(start, elapsed));
      m_bufferBlock2.LinkTo(m_actionBlock);

      FillBuffers();
    }

    private void BlockAction(int item)
    {
      Console.WriteLine(item);
      Thread.Sleep(2000);
    }

    private void FillBuffers()
    {
      for (int i = 0; i < 1000; i++)
      {
        if (i % 2 == 0)
        {
          m_bufferBlock1.Post(i);
        }
        else
        {
          m_bufferBlock2.Post(i);
        }
      }
    }

    private bool IsTimeElapsed(DateTime start, TimeSpan elapsed)
    {
      Console.WriteLine("checking time elapsed");
      return DateTime.Now > (start + elapsed);
    }

    public async Task Start()
    {
      await m_actionBlock.Completion;
    }
  }

代码设置了一个测试管道,并用奇数和偶数填充两个缓冲区。两个 BufferBlock 都连接到一个 ActionBlock,它只打印“已处理”数字并等待 2 秒。

m_bufferBlock1 和 m_actionBlock 之间的过滤条件检查(用于测试目的)自我们开始整个事情以来是否经过了 1 分钟。

如果我们运行它,它会生成以下输出:

1
checking time elapsed
3
5
7
9
11
13
15
17
19

正如我们所见,ActionBlock 从没有过滤器的 BufferBlock 中获取第一个元素,然后尝试从带有过滤器的 BufferBlock 中获取一个元素。过滤器评估为假,它会继续从没有过滤器的块中获取所有元素。

我的期望是,在处理了没有过滤器的 BufferBlock 中的一个元素后,它会尝试再次从另一个带有过滤器的 BufferBlock 中获取元素,再次对其进行评估。

这将是我预期(或期望)的结果:

1
checking time elapsed
3
checking time elapsed
5
checking time elapsed
7
checking time elapsed
9
checking time elapsed
11
checking time elapsed
13
checking time elapsed
15
// after timer has elapsed take elements also from other buffer
2
17
4
19

我现在的问题是,有没有办法“重置”已经“拒绝”的消息,以便再次对其进行评估,或者是否有另一种方法通过不同的方式对其进行建模?概括地说,它们是否真的从两个缓冲区中严格交替地拉出来并不重要! (因为我知道这取决于调度,如果不时有来自同一块的 2 个项目出队,那完全没问题) 但重要的是,不能丢弃或重新排队“拒绝”消息,因为一个缓冲区内的顺序很重要。

提前谢谢你

【问题讨论】:

    标签: c# task-parallel-library pipeline tpl-dataflow


    【解决方案1】:

    一个想法是定期或按需刷新两个块之间的链接。实现一个可定期刷新的LinkTo 并不是很困难。这是一个实现:

    public static IDisposable LinkTo<TOutput>(this ISourceBlock<TOutput> source,
        ITargetBlock<TOutput> target, Predicate<TOutput> predicate,
        TimeSpan refreshInterval, DataflowLinkOptions linkOptions = null)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (target == null) throw new ArgumentNullException(nameof(target));
        if (predicate == null) throw new ArgumentNullException(nameof(predicate));
        if (refreshInterval < TimeSpan.Zero)
            throw new ArgumentOutOfRangeException(nameof(refreshInterval));
        linkOptions = linkOptions ?? new DataflowLinkOptions();
    
        var locker = new object();
        var cts = new CancellationTokenSource();
        var token = cts.Token;
        var currentLink = source.LinkTo(target, linkOptions, predicate);
        var loopTask = Task.Run(async () =>
        {
            try
            {
                while (true)
                {
                    await Task.Delay(refreshInterval, token).ConfigureAwait(false);
                    currentLink.Dispose();
                    currentLink = source.LinkTo(target, linkOptions, predicate);
                }
            }
            finally
            {
                lock (locker) { cts.Dispose(); cts = null; }
            }
        }, token);
    
        _ = Task.Factory.ContinueWhenAny(new[] { source.Completion, target.Completion },
            _ => { lock (locker) cts?.Cancel(); }, token, TaskContinuationOptions.None,
            TaskScheduler.Default);
    
        return new Unlinker(() =>
        {
            lock (locker) cts?.Cancel();
            // Wait synchronously the task to complete, ignoring cancellation exceptions.
            try { loopTask.GetAwaiter().GetResult(); } catch (OperationCanceledException) { }
            currentLink.Dispose();
        });
    }
    
    private struct Unlinker : IDisposable
    {
        private readonly Action _action;
        public Unlinker(Action disposeAction) => _action = disposeAction;
        void IDisposable.Dispose() => _action?.Invoke();
    }
    

    使用示例:

    m_bufferBlock1.LinkTo(m_actionBlock, x => IsTimeElapsed(start, elapsed),
        refreshInterval: TimeSpan.FromSeconds(10));
    

    m_bufferBlock1m_actionBlock 之间的链接将每 10 秒刷新一次,直到两个块之一完成。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-30
      • 2015-06-07
      • 1970-01-01
      • 1970-01-01
      • 2013-10-16
      • 1970-01-01
      • 2021-10-29
      • 1970-01-01
      相关资源
      最近更新 更多