考虑以下示例:
class Program
{
private static readonly ITargetBlock<string> Mesh = CreateMesh();
private static readonly AsyncLocal<string> AsyncLocalContext
= new AsyncLocal<string>();
static async Task Main(string[] args)
{
var tasks = Enumerable.Range(1, 4)
.Select(ProcessMessage);
await Task.WhenAll(tasks);
Mesh.Complete();
await Mesh.Completion;
Console.WriteLine();
Console.WriteLine("Done");
}
private static async Task ProcessMessage(int number)
{
var param = number.ToString();
using (SetScopedAsyncLocal(param))
{
Console.WriteLine($"Before send {param}");
await Mesh.SendAsync(param);
Console.WriteLine($"After send {param}");
}
}
private static IDisposable SetScopedAsyncLocal(string value)
{
AsyncLocalContext.Value = value;
return new Disposer(() => AsyncLocalContext.Value = null);
}
private static ITargetBlock<string> CreateMesh()
{
var blockOptions = new ExecutionDataflowBlockOptions
{
BoundedCapacity = DataflowBlockOptions.Unbounded,
EnsureOrdered = false,
MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded
};
var block1 = new TransformBlock<string, string>(async input =>
{
await Task.Yield();
Console.WriteLine(
$" Block1 [thread {Thread.CurrentThread.ManagedThreadId}]" +
$" Input: {input} - Context: {AsyncLocalContext.Value}.");
return input;
}, blockOptions);
var block2 = new TransformBlock<string, string>(async input =>
{
await Task.Yield();
Console.WriteLine(
$" Block2 [thread {Thread.CurrentThread.ManagedThreadId}]" +
$" Input: {input} - Context: {AsyncLocalContext.Value}.");
return input;
}, blockOptions);
var block3 = new ActionBlock<string>(async input =>
{
await Task.Yield();
Console.WriteLine(
$" Block3 [thread {Thread.CurrentThread.ManagedThreadId}]" +
$" Input: {input} - Context: {AsyncLocalContext.Value}.");
}, blockOptions);
var linkOptions = new DataflowLinkOptions {PropagateCompletion = true};
block1.LinkTo(block2, linkOptions);
block2.LinkTo(block3, linkOptions);
return new EncapsulatedActionBlock<string>(block1, block3.Completion);
}
}
internal class EncapsulatedActionBlock<T> : ITargetBlock<T>
{
private readonly ITargetBlock<T> _wrapped;
public EncapsulatedActionBlock(ITargetBlock<T> wrapped, Task completion)
{
_wrapped = wrapped;
Completion = completion;
}
public DataflowMessageStatus OfferMessage(DataflowMessageHeader messageHeader,
T messageValue, ISourceBlock<T> source, bool consumeToAccept) =>
_wrapped.OfferMessage(messageHeader, messageValue, source, consumeToAccept);
public void Complete() => _wrapped.Complete();
public void Fault(Exception exception) => _wrapped.Fault(exception);
public Task Completion { get; }
}
internal class Disposer : IDisposable
{
private readonly Action _disposeAction;
public Disposer(Action disposeAction)
{
_disposeAction = disposeAction
?? throw new ArgumentNullException(nameof(disposeAction));
}
public void Dispose()
{
_disposeAction();
}
}
执行的结果将类似于:
Before send 1 After send 1 Before send 2 After send 2 Before send 3 After send 3 Before send 4 After send 4 Block1 [thread 9] Input: 3 - Context: 3. Block1 [thread 10] Input: 2 - Context: 1. Block1 [thread 8] Input: 4 - Context: 4. Block1 [thread 11] Input: 1 - Context: 2. Block2 [thread 9] Input: 2 - Context: 3. Block2 [thread 7] Input: 1 - Context: 2. Block2 [thread 10] Input: 3 - Context: 3. Block2 [thread 8] Input: 4 - Context: 4. Block3 [thread 11] Input: 4 - Context: 4. Block3 [thread 7] Input: 1 - Context: 2. Block3 [thread 9] Input: 3 - Context: 3. Block3 [thread 4] Input: 2 - Context: 3. Done
如您所见,传递到第二个TDF块后,传递的上下文值和存储的上下文值并不总是相同的。此行为搞砸了多个Logging框架的LogContext功能用法。
答案 0 :(得分:2)
要了解正在发生的事情,您必须了解Dataflow块如何工作。它们内部没有阻塞的线程,等待消息到达。该处理由工作人员任务完成。让我们考虑一下MaxDegreeOfParallelism = 1
的简单(默认)情况。最初有零个工作任务。使用SendAsync
异步发布消息时,发布该消息的同一任务将成为工作程序任务并开始处理该消息。如果在处理第一条消息时发布了另一条消息,则会发生其他情况。第二条消息将排队在块的输入队列中,并且发布该消息的任务将完成。第二条消息将由处理第一条消息的工作程序任务处理。只要队列中有消息排队,初始工作程序任务就会选择它们并逐一处理它们。如果在某个时刻没有更多的缓冲消息,则工作任务将完成,并且该块将返回其初始状态(零工作任务)。下一个SendAsync
将成为新的工作程序任务,依此类推。使用MaxDegreeOfParallelism = 1
,在任何给定时刻只能存在一个工作任务。
让我们用一个例子来说明这一点。以下是ActionBlock
,它以延迟X馈入,并以延迟Y处理每个消息。
private static void ActionBlockTest(int sendDelay, int processDelay)
{
Console.WriteLine($"SendDelay: {sendDelay}, ProcessDelay: {processDelay}");
var asyncLocal = new AsyncLocal<int>();
var actionBlock = new ActionBlock<int>(async i =>
{
await Task.Delay(processDelay);
Console.WriteLine($"Processed {i}, Context: {asyncLocal.Value}");
});
Task.Run(async () =>
{
foreach (var i in Enumerable.Range(1, 5))
{
asyncLocal.Value = i;
await actionBlock.SendAsync(i);
await Task.Delay(sendDelay);
}
}).Wait();
actionBlock.Complete();
actionBlock.Completion.Wait();
}
让我们看看如果我们快速发送消息并缓慢处理它们会发生什么情况:
ActionBlockTest(100, 200); // .NET Core 3.0
SendDelay:100,ProcessDelay:200
已处理1,内容:1
已处理2,内容:1
已处理3,上下文:1
已处理4,内容:1
已处理5,上下文:1
AsyncLocal
上下文保持不变,因为相同的工作程序任务处理了所有消息。
现在,让我们缓慢发送消息并快速处理它们:
ActionBlockTest(200, 100); // .NET Core 3.0
SendDelay:200,ProcessDelay:100
已处理1,内容:1
已处理2,内容:2
已处理3,上下文:3
已处理4,内容:4
已处理5,上下文:5
每条消息的AsyncLocal
上下文是不同的,因为每条消息都是由不同的工作程序任务处理的。
这个故事的道德教训是,每个SendAsync
都不能保证在消息行进到管道结束之前都创建一个遵循消息的异步工作流。因此AsyncLocal
类不能用于保存每条消息的环境数据。