C# 在TPL中清理CallContext

C# 在TPL中清理CallContext,c#,asynchronous,task-parallel-library,async-await,task,C#,Asynchronous,Task Parallel Library,Async Await,Task,根据我使用的是基于async/await的代码还是基于TPL的代码,在清理logicalCallContext时会出现两种不同的行为 如果使用以下异步/等待代码,我可以完全按照预期设置和清除逻辑CallContext: class Program { static async Task DoSomething() { CallContext.LogicalSetData("hello", "world"); await Task.Run(() =

根据我使用的是基于async/await的代码还是基于TPL的代码,在清理logical
CallContext
时会出现两种不同的行为

如果使用以下异步/等待代码,我可以完全按照预期设置和清除逻辑
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像异步/等待代码一样“释放”逻辑
CallContext

我对
CallContext
的替代方案不感兴趣


我希望修复上面的TPL/TAP代码,以便在针对.NET4.0框架的项目中使用它。如果在.NET4.0中不可能做到这一点,我仍然很好奇在.NET4.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 = }
async
方法中,写入时复制
CallContext

当异步方法启动时,它通知其逻辑调用上下文以激活写时复制行为。这意味着当前逻辑调用上下文并未实际更改,但已对其进行标记,这样,如果您的代码确实调用了
CallContext.LogicalSetData
,则逻辑调用上下文数据将在更改之前复制到新的当前逻辑调用上下文中

这意味着在您的
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中添加的)之外的所有代码都在同一线程上运行,具有相同的
CallContext。如果您想清理它,您需要在该上下文中(而不是在续文中)执行该操作:

您甚至可以从中抽象出一个范围,以确保您总是在自己之后进行清理:

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

在您的TPL版本中,是否存在逻辑CallContext在Task.Factory.StartNew有机会捕获之前被释放的风险?我还需要确保Task.Factory.StartNew中的所有continuations(如果有)确实都有CallContext,即使它被主线程“释放”。@BrentArias您可以使用thread.Sleep(我做过)来测试它。Task.Factory.StartNew as do Task.运行捕获(复制)上下文并将其存储在任务中,这样您就不必担心它了。你在这里有更多信息:@BrentArias使用Task.Run时,对Run的调用从调用线程捕获ExecutionContext,并将该ExecutionContext实例存储到任务对象中。当提供给Task.Run的委托稍后作为该任务执行的一部分被调用时,它是通过ExecutionContext.Run使用存储的上下文来完成的。对于Task.Run、ThreadPool.QueueUserWorkItem、Delegate.BeginInvoke、Stream.BeginRead、DispatcherSynchronizationContext.Post以及您能想到的任何其他异步API,都是如此。“我希望你能在一个项目上提供意见。
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;
        }
    }
}