【问题标题】:AsyncLocal values not correct with TPL DataflowTPL 数据流的 AsyncLocal 值不正确
【发布时间】:2019-10-01 06:44:30
【问题描述】:

考虑这个例子:

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();
    }
}

执行的结果会是这样的:

发送前 1 发送后 1 发送前 2 发送后 2 发送前 3 发送后 3 发送前 4 发送后 4 Block1 [线程 9] 输入:3 - 上下文:3。 Block1 [线程 10] 输入:2 - 上下文:1。 Block1 [线程 8] 输入:4 - 上下文:4。 Block1 [线程 11] 输入:1 - 上下文:2。 Block2 [线程 9] 输入:2 - 上下文:3。 Block2 [线程 7] 输入:1 - 上下文:2。 Block2 [线程 10] 输入:3 - 上下文:3。 Block2 [线程 8] 输入:4 - 上下文:4。 Block3 [线程 11] 输入:4 - 上下文:4。 Block3 [线程 7] 输入:1 - 上下文:2。 Block3 [线程 9] 输入:3 - 上下文:3。 Block3 [线程 4] 输入:2 - 上下文:3。 完毕

正如您所见,在移动到第二个 TDF 块后,传递的上下文值和存储的值并不总是相同。这种行为搞砸了多个 Logging 框架的 LogContext 功能使用。

  1. 这是预期的行为(请解释原因)?
  2. TPL 数据流是否会以某种方式混淆执行上下文?

【问题讨论】:

    标签: c# .net .net-core tpl-dataflow


    【解决方案1】:

    要了解发生了什么,您必须了解 Dataflow 块的工作原理。它们内部没有阻塞线程,等待消息到达。处理由工作任务完成。让我们考虑MaxDegreeOfParallelism = 1 的简单(和默认)情况。最初有零个工作任务。当使用SendAsync 异步发布消息时,发布消息的同一任务将成为工作任务并开始处理消息。如果在处理第一条消息时发布了另一条消息,则会发生其他事情。第二条消息将在块的输入队列中排队,发布它的任务将完成。第二条消息将由处理第一条消息的工作任务处理。只要队列中有消息入队,初始的worker任务就会挑选出来并一一处理。如果在某个时刻没有更多缓冲消息,则工作任务将完成,并且块将返回其初始状态(零工作任务)。下一个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
    

    发送延迟:100,处理延迟:200
    已处理 1,上下文:1
    已处理 2,上下文:1
    已处理 3,上下文:1
    已处理 4,上下文:1
    已处理 5,上下文:1

    AsyncLocal 上下文保持不变,因为同一个工作任务处理了所有消息。

    现在让我们慢慢发送消息并快速处理它们:

    ActionBlockTest(200, 100); // .NET Core 3.0
    

    发送延迟:200,处理延迟:100
    已处理 1,上下文:1
    已处理 2,上下文:2
    已处理 3,上下文:3
    已处理 4,上下文:4
    已处理 5,上下文:5

    AsyncLocal 上下文对于每条消息都是不同的,因为每条消息都由不同的工作任务处理。

    这个故事的道德教训是,每个SendAsync 不保证创建一个单一的异步工作流,该工作流跟随消息直到其旅程结束,直到管道结束。所以AsyncLocal 类不能用于保存每条消息的环境数据。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多