考虑以下示例:

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功能用法。


这是预期的行为(请解释原因)吗?
TPL数据流是否以某种方式弄乱了执行上下文?

最佳答案

要了解正在发生的事情,您必须了解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类不能用于保存每条消息的环境数据。

08-06 13:10