我有一系列TPL Dataflow块,希望观察系统内部的进展。
我知道我可以将TransformBlock
插入到我想要观察的网格中,让它发布到某种进度更新程序,然后将消息保持不变到下一个块。我不喜欢这个解决方案,因为块的纯粹是因为它的副作用,我还必须在任何我想要观察的地方更改块链接逻辑。
所以我想知道我是否可以使用ISourceBlock<T>.AsObservable
来观察在网格中传递消息而不改变它和而不消费消息。如果有效,这似乎是一个更纯粹,更实用的解决方案。
从我对Rx的理解(有限)意味着我需要observable是热而不是冷,以便我的progress
更新程序看到消息但不消耗它。 .Publish().RefCount()
似乎是让观察变得热烈的方式。但是,它根本无法正常工作 - 而是block2
或progress
接收并使用每条消息。
// 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}替换LinkTo
和block1
之间的block2
,这可能有效,但LinkTo
有下游BoundedCapacity = 1
这就是我首先使用TPL Dataflow的全部原因。
修改 一些澄清:
BoundedCapacity=1
。虽然在这个简单的例子中没有必要,但下游约束的情况是我发现TPL Dataflow非常有用的地方。为了澄清我在第二段中拒绝的解决方案,可以添加在block1和block2之间链接的以下块:
var progressBlock = new TransformBlock<int, int>( i => {SomeUpdateProgressMethod(i); return i;});
我还想保持背压,以便如果进一步上游的区块将工作分配给block1
以及其他同等工作人员,则不会将工作发送到block1
如果那条链已经忙了。
答案 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)
创建可观察的数据流块时要考虑两个选项。您可以:
这两个选项各有利弊。第一个选项提供及时但无序的通知。第二个选项提供有序但延迟的通知,并且还必须处理块对块链接的可处理性。如果在块完成之前手动设置两个块之间的链接,那么可观察对象会发生什么?
下面是第一个选项的实现,它创建一个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
,不适合中间人。