清理TPL中的CallContext

时间:2015-03-12 03:12:09

标签: c# asynchronous task-parallel-library async-await task

根据我是使用基于异步/等待的代码还是基于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以与async / await代码相同的方式“释放”逻辑CallContext吗?

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

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

2 个答案:

答案 0 :(得分:11)

async方法中,CallContext将被复制到write:

  

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

来自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")
        }));
}

CallContext中的Main不会包含"hello"广告位:

  

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

在TPL等效项中,Task.Run之外的所有代码(Task.Factory.StartNew都应该Task.Run添加到。{4.5}中)在同一个线程上运行,完全相同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;
        }
    }
}

答案 1 :(得分:4)

一个好问题。 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()。并且在async之后它不存在,因为它首先不存在于它之前。

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

OTOH,对于非异步版本,没有"逻辑"范围并且此处没有外部Task.Run,因此CallContext.LogicalSetData("hello", "world")的写时复制克隆成为Task.Run线程的当前克隆(但是延续和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; } lambdas仍然得到它们自己的克隆)。因此,您需要在{{1}} lambda中移动{{1}},或手动克隆上下文:

{{1}}