簡體   English   中英

清理TPL中的CallContext

[英]Cleaning up CallContext in TPL

根據我是使用基於異步/等待的代碼還是基於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中完成。

async方法中, CallContext在寫入時被復制:

當異步方法啟動時,它會通知其邏輯調用上下文以激活寫時復制行為。 這意味着當前的邏輯調用上下文實際上沒有更改,但它被標記為如果您的代碼調用CallContext.LogicalSetData ,則邏輯調用上下文數據在更改之前將被復制到新的當前邏輯調用上下文中。

來自隱式異步上下文(“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")
        }));
}

MainCallContext不包含"hello"插槽:

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

在TPL等價物中, Task.Run之外的所有代碼(應該是Task.Factory.StartNew作為在.Net 4.5中添加的Task.Run )在具有相同精確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;
        }
    }
}

一個好問題。 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()之后它就不存在了DoSomething().Wait()因為它首先不在它之前。

有趣的是, DoSomethingasync版本在第一個LogicalSetData為其作用域創建了LogicalCallContext的寫時復制克隆。 即使內部沒有異步,它也await Task.FromResult(0) - 嘗試await Task.FromResult(0) 我假設在第一次寫操作時,整個ExecutionContext被克隆為async方法的范圍。

OTOH,對於非異步版本沒有“邏輯”范圍,沒有外部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;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM