清理 TPL 中的 CallContext

Cleaning up CallContext in TPL

根据我使用的是基于 async/await 的代码还是基于 TPL 的代码,我得到了关于逻辑 CallContext.

清理的两种不同行为

如果我使用以下 async/await 代码,我可以完全按照预期设置和清除逻辑 CallContext

class Program
{
    static async Task DoSomething()
    {
        CallContext.LogicalSetData("hello", "world");

        await Task.Run(() =>
            Debug.WriteLine(new
            {
                Place = "Task.Run",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            }))
            .ContinueWith((t) =>
                CallContext.FreeNamedDataSlot("hello")
                );

        return;
    }

    static void Main(string[] args)
    {
        DoSomething().Wait();

        Debug.WriteLine(new
        {
            Place = "Main",
            Id = Thread.CurrentThread.ManagedThreadId,
            Msg = CallContext.LogicalGetData("hello")
        });

    }
}

以上输出如下:

{ Place = Task.Run, Id = 9, Msg = world }
{ Place = Main, Id = 8, Msg = }

注意 Msg =,它表示主线程上的 CallContext 已被释放并且为空。

但是当我切换到纯 TPL/TAP 代码时我无法达到同样的效果...

class Program
{
    static Task DoSomething()
    {
        CallContext.LogicalSetData("hello", "world");

        var result = Task.Run(() =>
            Debug.WriteLine(new
            {
                Place = "Task.Run",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            }))
            .ContinueWith((t) =>
                CallContext.FreeNamedDataSlot("hello")
                );

        return result;
    }

    static void Main(string[] args)
    {
        DoSomething().Wait();

        Debug.WriteLine(new
        {
            Place = "Main",
            Id = Thread.CurrentThread.ManagedThreadId,
            Msg = CallContext.LogicalGetData("hello")
        });
    }
}

以上输出如下:

{ Place = Task.Run, Id = 10, Msg = world }
{ Place = Main, Id = 9, Msg = world }

我能做些什么来强制 TPL 以 "free" 逻辑 CallContext 与 async/await 代码相同的方式吗?

我对 CallContext 的替代品不感兴趣。

我希望修复上面的 TPL/TAP 代码,以便我可以在针对 .net 4.0 框架的项目中使用它。如果这在 .net 4.0 中是不可能的,我仍然很好奇它是否可以在 .net 4.5 中完成。

好问题。 await 版本可能不会像您在这里认为的那样工作。让我们在 DoSomething:

中添加另一个日志行
class Program
{
    static async Task DoSomething()
    {
        CallContext.LogicalSetData("hello", "world");

        await Task.Run(() =>
            Debug.WriteLine(new
            {
                Place = "Task.Run",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            }))
            .ContinueWith((t) =>
                CallContext.FreeNamedDataSlot("hello")
                );

        Debug.WriteLine(new
        {
            Place = "after await",
            Id = Thread.CurrentThread.ManagedThreadId,
            Msg = CallContext.LogicalGetData("hello")
        });
    }

    static void Main(string[] args)
    {

        DoSomething().Wait();

        Debug.WriteLine(new
        {
            Place = "Main",
            Id = Thread.CurrentThread.ManagedThreadId,
            Msg = CallContext.LogicalGetData("hello")
        });

        Console.ReadLine();
    }
}

输出:

{ Place = Task.Run, Id = 10, Msg = world }
{ Place = after await, Id = 11, Msg = world }
{ Place = Main, Id = 9, Msg =  }

请注意 "world"await 之后仍然存在,因为它在 await 之前存在。它在 DoSomething().Wait() 之后不存在,因为它首先不存在。

有趣的是,DoSomethingasync 版本在第一个 LogicalSetData 上为其范围创建了 LogicalCallContext 的写时复制克隆。即使内部没有异步,它也会这样做——试试 await Task.FromResult(0)。我假设在第一次写入操作时,整个 ExecutionContext 被克隆到 async 方法的范围内。

OTOH,对于非异步版本,这里没有 "logical" 作用域,也没有外部 ExecutionContext,因此 ExecutionContext 的写时复制克隆成为当前版本Main 线程(但延续和 Task.Run lambda 仍然有自己的克隆)。因此,您需要在 Task.Run lambda 中移动 CallContext.LogicalSetData("hello", "world"),或者手动克隆上下文:

static Task DoSomething()
{
    var ec = ExecutionContext.Capture();
    Task task = null;
    ExecutionContext.Run(ec, _ =>
    {
        CallContext.LogicalSetData("hello", "world");

        var result = Task.Run(() =>
            Debug.WriteLine(new
            {
                Place = "Task.Run",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            }))
            .ContinueWith((t) =>
                CallContext.FreeNamedDataSlot("hello")
                );

        task = result;
    }, null);

    return task;
}

async 方法中,CallContext 在写入时被复制:

When an async method starts, it notifies its logical call context to activate copy-on-write behavior. This means the current logical call context is not actually changed, but it is marked so that if your code does call CallContext.LogicalSetData, the logical call context data is copied into a new current logical call context before it is changed.

来自Implicit Async Context ("AsyncLocal")

这意味着在您的 async 版本中 CallContext.FreeNamedDataSlot("hello") 延续是多余的 即使没有它也是如此:

static async Task DoSomething()
{
    CallContext.LogicalSetData("hello", "world");

    await Task.Run(() =>
        Console.WriteLine(new
        {
            Place = "Task.Run",
            Id = Thread.CurrentThread.ManagedThreadId,
            Msg = CallContext.LogicalGetData("hello")
        }));
}

Main 中的 CallContext 不包含 "hello" 槽:

{ Place = Task.Run, Id = 3, Msg = world }
{ Place = Main, Id = 1, Msg = }

在等效的 TPL 中,Task.Run 之外的所有代码(应该是 Task.Factory.StartNew,因为 Task.Run 是在 .Net 4.5 中添加的)与 [=43 在同一个线程上运行=]完全相同 CallContext。如果你想清理它,你需要在那个上下文中(而不是在后续中)进行清理:

static Task DoSomething()
{
    CallContext.LogicalSetData("hello", "world");

    var result = Task.Factory.StartNew(() =>
        Debug.WriteLine(new
        {
            Place = "Task.Run",
            Id = Thread.CurrentThread.ManagedThreadId,
            Msg = CallContext.LogicalGetData("hello")
        }));

    CallContext.FreeNamedDataSlot("hello");
    return result;
}

您甚至可以从中抽象出一个作用域,以确保您总是自己清理:

static Task DoSomething()
{
    using (CallContextScope.Start("hello", "world"))
    {
        return Task.Factory.StartNew(() =>
            Debug.WriteLine(new
            {
                Place = "Task.Run",
                Id = Thread.CurrentThread.ManagedThreadId,
                Msg = CallContext.LogicalGetData("hello")
            }));
    }
}

使用:

public static class CallContextScope
{
    public static IDisposable Start(string name, object data)
    {
        CallContext.LogicalSetData(name, data);
        return new Cleaner(name);
    }

    private class Cleaner : IDisposable
    {
        private readonly string _name;
        private bool _isDisposed;

        public Cleaner(string name)
        {
            _name = name;
        }

        public void Dispose()
        {
            if (_isDisposed)
            {
                return;
            }

            CallContext.FreeNamedDataSlot(_name);
            _isDisposed = true;
        }
    }
}