使用AsObservable来观察TPL Dataflow块而不消耗消息

时间:2017-06-16 01:43:22

标签: c# .net system.reactive tpl-dataflow rx.net

我有一系列TPL Dataflow块,希望观察系统内部的进展。

我知道我可以将TransformBlock插入到我想要观察的网格中,让它发布到某种进度更新程序,然后将消息保持不变到下一个块。我不喜欢这个解决方案,因为块的纯粹是因为它的副作用,我还必须在任何我想要观察的地方更改块链接逻辑。

所以我想知道我是否可以使用ISourceBlock<T>.AsObservable来观察在网格中传递消息而不改变它而不消费消息。如果有效,这似乎是一个更纯粹,更实用的解决方案。

从我对Rx的理解(有限)意味着我需要observable是热而不是冷,以便我的progress更新程序看到消息但不消耗它。 .Publish().RefCount()似乎是让观察变得热烈的方式。但是,它根本无法正常工作 - 而是block2progress接收并使用每条消息。

// Set up mesh
var block1 = new TransformBlock<int, int>(i => i + 20, new ExecutionDataflowBlockOptions() { BoundedCapacity = 1 });
var block2 = new ActionBlock<int>(i => Debug.Print("block2:" + i.ToString()), new ExecutionDataflowBlockOptions() { BoundedCapacity = 1 }); 
var obs = block1.AsObservable().Publish().RefCount(); // Declare this here just in case it makes a difference to do it before the LinkTo call.
var l1 = block1.LinkTo(block2, new DataflowLinkOptions() { PropagateCompletion = true});

// Progress
obs.ForEachAsync(i => Debug.Print("progress:" + i.ToString()));

// Start
var vals = Enumerable.Range(1, 5);
foreach (var v in vals)
{
    block1.Post(v);
}
block1.Complete();

结果是不确定的,但我得到的东西是这样的:

block2:21
progress:22
progress:24
block2:23
progress:25

所以,我做错了什么,或者由于TPL Dataflow AsObservable的实现方式,这是不可能的?

我意识到我还可以用{Oberable / Observer}替换LinkToblock1之间的block2,这可能有效,但LinkTo有下游BoundedCapacity = 1这就是我首先使用TPL Dataflow的全部原因。

修改 一些澄清:

  • 我打算在block2中设置BoundedCapacity=1。虽然在这个简单的例子中没有必要,但下游约束的情况是我发现TPL Dataflow非常有用的地方。
  • 为了澄清我在第二段中拒绝的解决方案,可以添加在block1和block2之间链接的以下块:

    var progressBlock = new TransformBlock<int, int>( i => {SomeUpdateProgressMethod(i); return i;});

  • 我还想保持背压,以便如果进一步上游的区块将工作分配给block1以及其他同等工作人员,则不会将工作发送到block1如果那条链已经忙了。

4 个答案:

答案 0 :(得分:4)

您的代码存在的问题是,您要为block1的两位消费者连接。然后,数据流只是给出了消费者首先存在的价值。

因此,您需要将block1中的值广播到另外两个块中,然后才能独立使用这些值。

只是旁注,不要.Publish().RefCount(),因为它没有按照您的想法行事。它将有效地使一个运行只能观察到,在一次运行期间将允许多个观察者连接并查看相同的值。它与数据源无关,也与Dataflow块如何交互无关。

试试这段代码:

// Set up mesh
var block1 = new TransformBlock<int, int>(i => i + 20);
var block_boadcast = new BroadcastBlock<int>(i => i, new DataflowBlockOptions());
var block_buffer = new System.Threading.Tasks.Dataflow.BufferBlock<int>();
var block2 = new ActionBlock<int>(i => Debug.Print("block2:" + i.ToString()));
var obs = block_buffer.AsObservable();
var l1 = block1.LinkTo(block_boadcast);
var l2 = block_boadcast.LinkTo(block2);
var l3 = block_boadcast.LinkTo(block_buffer);

// Progress
obs.Subscribe(i => Debug.Print("progress:" + i.ToString()));

// Start
var vals = Enumerable.Range(1, 5);
foreach (var v in vals)
{
    block1.Post(v);
}
block1.Complete();

这让我:

block2:21
block2:22
block2:23
block2:24
block2:25
progress:21
progress:22
progress:23
progress:24
progress:25

我认为你想要的是什么。

现在,除此之外,使用Rx可能是一个更好的选择。它比任何TPL或Dataflow选项都更强大,更具说服力。

您的代码归结为:

Observable
    .Range(1, 5)
    .Select(i => i + 20)
    .Do(i => Debug.Print("progress:" + i.ToString()));
    .Subscribe(i => Debug.Print("block2:" + i.ToString()));

这几乎可以给你相同的结果。

答案 1 :(得分:1)

创建可观察的数据流块时要考虑两个选项。您可以:

    每次处理邮件时
  1. 发出通知,或者
  2. 链接的块接受存储在块输出缓冲区中的先前处理的消息时发出通知。

这两个选项各有利弊。第一个选项提供及时但无序的通知。第二个选项提供有序但延迟的通知,并且还必须处理块对块链接的可处理性。如果在块完成之前手动设置两个块之间的链接,那么可观察对象会发生什么?

下面是第一个选项的实现,它创建一个TransformBlock以及该块的非消耗IObservable。在第一个实现的基础上,还有一个等效于ActionBlock的实现(尽管由于代码不那么多,它也可以通过复制粘贴和改编TransformBlock实现来独立实现)。

public static TransformBlock<TInput, TOutput>
    CreateObservableTransformBlock<TInput, TOutput>(
    Func<TInput, Task<TOutput>> transform,
    out IObservable<(TInput Input, TOutput Output,
        int StartedIndex, int CompletedIndex)> observable,
    ExecutionDataflowBlockOptions dataflowBlockOptions = null)
{
    if (transform == null) throw new ArgumentNullException(nameof(transform));
    dataflowBlockOptions = dataflowBlockOptions ?? new ExecutionDataflowBlockOptions();

    var semaphore = new SemaphoreSlim(1);
    int startedIndexSeed = 0;
    int completedIndexSeed = 0;

    var notificationsBlock = new BufferBlock<(TInput, TOutput, int, int)>(
        new DataflowBlockOptions() { BoundedCapacity = 100 });

    var transformBlock = new TransformBlock<TInput, TOutput>(async item =>
    {
        var startedIndex = Interlocked.Increment(ref startedIndexSeed);
        var result = await transform(item).ConfigureAwait(false);
        await semaphore.WaitAsync().ConfigureAwait(false);
        try
        {
            // Send the notifications in synchronized fashion
            var completedIndex = Interlocked.Increment(ref completedIndexSeed);
            await notificationsBlock.SendAsync(
                (item, result, startedIndex, completedIndex)).ConfigureAwait(false);
        }
        finally
        {
            semaphore.Release();
        }
        return result;
    }, dataflowBlockOptions);

    _ = transformBlock.Completion.ContinueWith(t =>
    {
        if (t.IsFaulted) ((IDataflowBlock)notificationsBlock).Fault(t.Exception);
        else notificationsBlock.Complete();
    }, TaskScheduler.Default);

    observable = notificationsBlock.AsObservable();
    // A dummy subscription to prevent buffering in case of no external subscription.
    observable.Subscribe(
        DataflowBlock.NullTarget<(TInput, TOutput, int, int)>().AsObserver());
    return transformBlock;
}

// Overload with synchronous lambda
public static TransformBlock<TInput, TOutput>
    CreateObservableTransformBlock<TInput, TOutput>(
    Func<TInput, TOutput> transform,
    out IObservable<(TInput Input, TOutput Output,
        int StartedIndex, int CompletedIndex)> observable,
    ExecutionDataflowBlockOptions dataflowBlockOptions = null)
{
    return CreateObservableTransformBlock(item => Task.FromResult(transform(item)),
        out observable, dataflowBlockOptions);
}

// ActionBlock equivalent (requires the System.Reactive package)
public static ITargetBlock<TInput>
    CreateObservableActionBlock<TInput>(
    Func<TInput, Task> action,
    out IObservable<(TInput Input, int StartedIndex, int CompletedIndex)> observable,
    ExecutionDataflowBlockOptions dataflowBlockOptions = null)
{
    if (action == null) throw new ArgumentNullException(nameof(action));
    var block = CreateObservableTransformBlock<TInput, object>(
        async item => { await action(item).ConfigureAwait(false); return null; },
        out var sourceObservable, dataflowBlockOptions);
    block.LinkTo(DataflowBlock.NullTarget<object>());
    observable = sourceObservable
        .Select(entry => (entry.Input, entry.StartedIndex, entry.CompletedIndex));
    return block;
}

// ActionBlock equivalent with synchronous lambda
public static ITargetBlock<TInput>
    CreateObservableActionBlock<TInput>(
    Action<TInput> action,
    out IObservable<(TInput Input, int StartedIndex, int CompletedIndex)> observable,
    ExecutionDataflowBlockOptions dataflowBlockOptions = null)
{
    return CreateObservableActionBlock(
        item => { action(item); return Task.CompletedTask; },
        out observable, dataflowBlockOptions);
}

Windows窗体中的用法示例:

private async void Button1_Click(object sender, EventArgs e)
{
    var block = CreateObservableTransformBlock((int i) => i + 20,
        out var observable,
        new ExecutionDataflowBlockOptions() { BoundedCapacity = 1 });

    var vals = Enumerable.Range(1, 20).ToList();
    TextBox1.Clear();
    ProgressBar1.Value = 0;

    observable.ObserveOn(SynchronizationContext.Current).Subscribe(onNext: x =>
    {
        TextBox1.AppendText($"Value {x.Input} transformed to {x.Output}\r\n");
        ProgressBar1.Value = (x.CompletedIndex * 100) / vals.Count;
    }, onError: ex =>
    {
        TextBox1.AppendText($"An exception occured: {ex.Message}\r\n");
    },
    onCompleted: () =>
    {
        TextBox1.AppendText("The job completed successfully\r\n");
    });

    block.LinkTo(DataflowBlock.NullTarget<int>());

    foreach (var i in vals) await block.SendAsync(i);
    block.Complete();
}

在上面的示例中,observable变量的类型为:

IObservable<(int Input, int Output, int StartedIndex, int CompletedIndex)>

两个索引均基于1。

答案 2 :(得分:0)

尝试更换:

obs.ForEachAsync(i => Debug.Print("progressBlock:" + i.ToString()));

使用:

obs.Subscribe(i => Debug.Print("progressBlock:" + i.ToString()));

我认为ForEachAsync方法没有在正确/正在触发时挂钩,但是异步部分正在进行一些时髦的事情。

答案 3 :(得分:0)

通过为链中的块指定BoundedCapacity,您可以创建一些情况,其中某些消息被目标块拒绝,因为ActionBlock的缓冲区已满,并且消息被拒绝。

通过从缓冲区块创建observable,您确实提供了竞争条件:您的数据有两个消费者同时获取消息。 TPL Dataflow中的数据块会将数据传播到第一个可用的使用者,这会导致您进入应用程序的不确定状态。

现在,回到你的问题。您可以引入BroadcastBlock,因为它为所有使用者提供复制数据,而不是唯一的第一个,但在这种情况下您必须删除缓冲区大小限制,广播块是就像一个电视频道,你不能得到以前的节目,你只有一个当前的节目。

附注:您不检查Post方法的返回值,您可以考虑使用await SendAsync,并且为了更好的限制效果,为起始点块设置BoundedCapacity,不适合中间人。