嵌套多线程操作跟踪

发布于 2024-08-29 06:46:32 字数 922 浏览 0 评论 0原文

我有一个类似的代码

void ExecuteTraced(Action a, string message)
{
    TraceOpStart(message);
    a();
    TraceOpEnd(message);
}

回调 (a) 可以再次调用 ExecuteTraced,并且在某些情况下,异步调用(通过 ThreadPool、BeginInvoke、PLINQ 等,因此我无法显式标记操作范围)。我想跟踪所有嵌套操作(即使它们异步执行)。因此,我需要能够在逻辑调用上下文中获取最后跟踪的操作(可能有很多并发线程,因此不可能使用 lastTraced 静态字段)。

有 CallContext.LogicalGetData 和 CallContext.LogicalSetData,但不幸的是,当调用 EndInvoke() 时,LogicalCallContext 将更改传播回父上下文。更糟糕的是,如果 EndInvoke() 被异步调用,这种情况可能随时发生。 EndInvoke 更改当前 CallContext - 为什么?

另外,还有 Trace.CorrelationManager,但它基于在 CallContext 上并且有同样的麻烦。

有一个解决方法:使用 CallContext.HostContext 属性,该属性不会在异步操作结束时传播回来。此外,它不会克隆,因此该值应该是不可变的 - 这不是问题。不过,它由 HttpContext 使用,因此解决方法在 Asp.Net 应用程序中不可用。

我看到的唯一方法是将 HostContext (如果不是我的)或整个 LogicalCallContext 包装到动态中,并分派最后跟踪操作旁边的所有调用。

I've a code alike

void ExecuteTraced(Action a, string message)
{
    TraceOpStart(message);
    a();
    TraceOpEnd(message);
}

The callback (a) could call ExecuteTraced again, and, in some cases, asynchronously (via ThreadPool, BeginInvoke, PLINQ etc, so I've no ability to explicitly mark operation scope). I want to trace all operation nested (even if they perform asynchronously). So, I need the ability to get last traced operation inside logical call context (there may be a lot of concurrent threads, so it's impossible to use lastTraced static field).

There're CallContext.LogicalGetData and CallContext.LogicalSetData, but unfortunately, LogicalCallContext propagates changes back to the parent context as EndInvoke() called. Even worse, this may occure at any moment if EndInvoke() was called async.
EndInvoke changes current CallContext - why?

Also, there is Trace.CorrelationManager, but it based on CallContext and have all the same troubles.

There's a workaround: use the CallContext.HostContext property which does not propagates back as async operation ended. Also, it does'nt clone, so the value should be immutable - not a problem. Though, it's used by HttpContext and so, workaround is not usable in Asp.Net apps.

The only way I see is to wrap HostContext (if not mine) or entire LogicalCallContext into dynamic and dispatch all calls beside last traced operation.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

凯凯我们等你回来 2024-09-05 06:46:32

好吧,我自己回答。

简短:没有解决方案。

稍微详细:

问题是,我需要一种方法来存储每个逻辑上下文的最后一个活动操作。跟踪代码无法控制执行流程,因此不可能将 lastStartedOperation 作为参数传递。调用上下文可能会克隆(例如,如果另一个线程启动),因此我需要将值克隆为上下文克隆。

CallContext.LogicalSetData() 很适合,但它会在异步操作结束时将值合并到原始上下文中(实际上,替换调用 EndInvoke 之前所做的所有更改)。从理论上讲,它甚至可能异步发生,从而给出 CallContext.LogicalGetData() 的不可预测的结果。

我说理论上是因为 asyncCallback 内的简单调用 a.EndInvoke() 不会替换原始上下文中的值。不过,我没有检查远程调用的行为(看起来,WCF 根本不尊重 CallContext)。另外,文档(旧文档)说:

BeginInvoke 方法传递
CallContext 到服务器。什么时候
调用EndInvoke方法时,
CallContext 被合并回
线。这包括以下情况
调用 BeginInvoke 和 EndInvoke
顺序和 BeginInvoke 所在的位置
在一个线程上调用,EndInvoke 是
调用回调函数。

最后一个版本不太明确:

BeginInvoke 方法传递
CallContext 到服务器。当
调用EndInvoke方法,获取数据
CallContext 中包含的内容被复制
回到调用的线程
开始调用。

如果您深入研究框架源代码,您会发现值实际上存储在当前线程的当前 ExecutionContext 内的 LogicalCallContext 内的哈希表中。

当调用上下文克隆时(例如在 BeginInvoke 上),调用 LogicalCallContext.Clone。 EndInvoke(至少在原始 CallContext 中调用时)调用 LogicalCallContext.Merge(),用新值替换 m_Datastore 中的旧值。

因此,我们需要以某种方式提供将被克隆但不会合并回来的值。

LogicalCallContext.Clone() 还克隆(不合并)两个私有字段 m_RemotingData 和 m_SecurityData 的内容。由于字段的类型定义为内部,因此您无法从它们派生(即使使用发出),添加属性 MyNoFlowbackValue 并将 m_RemotingData (或另一个)字段的值替换为派生类的实例。

此外,字段的类型不是从 MBR 派生的,因此不可能使用透明代理来包装它们。

您无法从 LogicalCallContext 继承 - 它是密封的。 (实际上,如果使用 CLR 分析 api 来替换 IL,就像模拟框架那样。这不是理想的解决方案。)您无法

替换 m_Datastore 值,因为 LogicalCallContext 仅序列化哈希表的内容,而不序列化哈希表本身。

最后的解决方案是使用 CallContext.HostContext。这有效地将数据存储在 LogicalCallContext 的 m_hostContext 字段中。 LogicalCallContext.Clone() 共享(而不是克隆) m_hostContext 的值,因此该值应该是不可变的。不过这不是问题。

如果使用 HttpContext,即使这样也会失败,因为它设置 CallContext.HostContext 属性来替换您的旧值。讽刺的是,HttpContext 没有实现 ILogicalThreadAffinative,因此不会存储为 m_hostContext 字段的值。它只是用 null 替换旧值。

因此,没有解决方案,也永远不会有解决方案,因为 CallContext 是远程处理的一部分,而远程处理已经过时了。

PS Thace.CorrelationManager 在内部使用 CallContext,因此也无法按预期工作。顺便说一句,LogicalCallContext 有特殊的解决方法来在上下文克隆上克隆 CorrelationManager 的操作堆栈。遗憾的是,它没有关于合并的特殊解决方法。完美的!

PPS 样本:

static void Main(string[] args)
{
    string key = "aaa";
    EventWaitHandle asyncStarted = new AutoResetEvent(false);
    IAsyncResult r = null;

    CallContext.LogicalSetData(key, "Root - op 0");
    Console.WriteLine("Initial: {0}", CallContext.LogicalGetData(key));

    Action a = () =>
    {
        CallContext.LogicalSetData(key, "Async - op 0");
        asyncStarted.Set();
    };
    r = a.BeginInvoke(null, null);

    asyncStarted.WaitOne();
    Console.WriteLine("AsyncOp started: {0}", CallContext.LogicalGetData(key));

    CallContext.LogicalSetData(key, "Root - op 1");
    Console.WriteLine("Current changed: {0}", CallContext.LogicalGetData(key));

    a.EndInvoke(r);
    Console.WriteLine("Async ended: {0}", CallContext.LogicalGetData(key));

    Console.ReadKey();
}

Ok, I'm answering myself.

Short one: there is no solution.

Slightly detailed:

The problem is, I need a way to store last active operation per each logical context. Tracing code will've no control over execution flow, so it's impossible to pass lastStartedOperation as a parameter. Call context may clone (e.g. if another thread started), so I need to clone the value as context clones.

CallContext.LogicalSetData() suits well, but it merges values into the original context as asynchronous operation ended (in effect, replacing all changes made before EndInvoke called). Theortically, it may occure even asynchronously, giving unpredictable result of CallContext.LogicalGetData().

I say theoretically because simple call a.EndInvoke() inside an asyncCallback does not replace values in the original context. Though, I did not check behavior of remoting calls (and it seems, WCF does not honor CallContext at all). Also, the documentation (old one) says:

The BeginInvoke method passes the
CallContext to the server. When
EndInvoke method is called, the
CallContext is merged back onto the
thread. This includes cases where
BeginInvoke and EndInvoke are called
sequentially and where BeginInvoke is
called on one thread and EndInvoke is
called on a callback function.

Last version is not so definite:

The BeginInvoke method passes the
CallContext to the server. When the
EndInvoke method is called, the data
contained in the CallContext is copied
back onto the thread that called
BeginInvoke.

If you digg into framework source, you'll find that values are actually stored inside a hashtable inside LogicalCallContext inside current ExecutionContext of the current thread.

When call context clones (e.g. on BeginInvoke) LogicalCallContext.Clone called. And EndInvoke (at least when called inside original CallContext) calls LogicalCallContext.Merge() replacing old values inside m_Datastore with new ones.

So we need somehow supply the value which will be cloned but not merged back.

LogicalCallContext.Clone() also clones (without merging) content of two private fields, m_RemotingData and m_SecurityData. As the field's types defined as internal, you could not derive from them (even with emit), add property MyNoFlowbackValue and replace m_RemotingData (or another one) field's value with instance of derived class.

Also, field's types are not derived from MBR, so it's impossible to wrap them using transparent proxy.

You could not inherit from LogicalCallContext - it's sealed. (N.B. actually, you could - if using CLR profiling api to replace IL as mock frameworks do. Not a desired solution.)

You could not replace the m_Datastore value, because LogicalCallContext serializes only content of the hashtable, not the hashtable itself.

Last solution is to use CallContext.HostContext. This effectively stores data in the m_hostContext field of the LogicalCallContext. LogicalCallContext.Clone() shares (not clones) the value of m_hostContext, so the value should be immutable. Not a problem though.

And even this fails if HttpContext used, as it sets CallContext.HostContext property replacing your old value. Ironically, HttpContext does not implement ILogicalThreadAffinative, and therefore does not stored as value of the m_hostContext field. It just replaces old value with null.

So, there's no solution and never will be, as CallContext is the part of remoting and remoting is obsolete.

P.S. Thace.CorrelationManager uses CallContext internally and therefore does not work as desired, too. BTW, LogicalCallContext has special workaround to clone CorrelationManager's operation stack on context clone. Sadly, it has not special workaround on merge. Perfect!

P.P.S. The sample:

static void Main(string[] args)
{
    string key = "aaa";
    EventWaitHandle asyncStarted = new AutoResetEvent(false);
    IAsyncResult r = null;

    CallContext.LogicalSetData(key, "Root - op 0");
    Console.WriteLine("Initial: {0}", CallContext.LogicalGetData(key));

    Action a = () =>
    {
        CallContext.LogicalSetData(key, "Async - op 0");
        asyncStarted.Set();
    };
    r = a.BeginInvoke(null, null);

    asyncStarted.WaitOne();
    Console.WriteLine("AsyncOp started: {0}", CallContext.LogicalGetData(key));

    CallContext.LogicalSetData(key, "Root - op 1");
    Console.WriteLine("Current changed: {0}", CallContext.LogicalGetData(key));

    a.EndInvoke(r);
    Console.WriteLine("Async ended: {0}", CallContext.LogicalGetData(key));

    Console.ReadKey();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文