Using AsObservable to observe TPL Dataflow blocks without consuming messages

前端 未结 4 1310
星月不相逢
星月不相逢 2021-01-18 06:54

I have a chain of TPL Dataflow blocks and would like to observe progress somewhere inside the system.

I am aware that I could just jam a TransformBlock

4条回答
  •  礼貌的吻别
    2021-01-18 07:12

    There are two options to consider when creating an observable dataflow block. You can either:

    1. emit a notification every time a message is processed, or
    2. emit a notification when a previously processed message stored in the block's output buffer, is accepted by a linked block.

    Both options have pros and cons. The first option provides timely but unordered notifications. The second option provides ordered but delayed notifications, and also must deal with the disposability of the block-to-block linking. What should happen with the observable, when the link between the two blocks is manually disposed before the blocks are completed?

    Below is an implementation of the first option, that creates a TransformBlock together with a non-consuming IObservable of this block. There is also an implementation for an ActionBlock equivalent, based on the first implementation (although it could also be implemented independently by copy-pasting and adapting the TransformBlock implementation, since the code is not that much).

    public static TransformBlock
        CreateObservableTransformBlock(
        Func> 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(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
        CreateObservableTransformBlock(
        Func 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
        CreateObservableActionBlock(
        Func action,
        out IObservable<(TInput Input, int StartedIndex, int CompletedIndex)> observable,
        ExecutionDataflowBlockOptions dataflowBlockOptions = null)
    {
        if (action == null) throw new ArgumentNullException(nameof(action));
        var block = CreateObservableTransformBlock(
            async item => { await action(item).ConfigureAwait(false); return null; },
            out var sourceObservable, dataflowBlockOptions);
        block.LinkTo(DataflowBlock.NullTarget());
        observable = sourceObservable
            .Select(entry => (entry.Input, entry.StartedIndex, entry.CompletedIndex));
        return block;
    }
    
    // ActionBlock equivalent with synchronous lambda
    public static ITargetBlock
        CreateObservableActionBlock(
        Action action,
        out IObservable<(TInput Input, int StartedIndex, int CompletedIndex)> observable,
        ExecutionDataflowBlockOptions dataflowBlockOptions = null)
    {
        return CreateObservableActionBlock(
            item => { action(item); return Task.CompletedTask; },
            out observable, dataflowBlockOptions);
    }
    
    

    Usage example in Windows Forms:

    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());
    
        foreach (var i in vals) await block.SendAsync(i);
        block.Complete();
    }
    

    In the above example the type of the observable variable is:

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

    The two indices are 1-based.

    提交回复
    热议问题