任务并行库中的任务如何影响ActivityID?
在使用任务并行库之前,我经常使用 CorrelationManager.ActivityId 来跟踪多线程的跟踪/错误报告。
ActivityId 存储在线程本地存储中,因此每个线程都有自己的副本。这个想法是,当您启动线程(活动)时,您分配一个新的 ActivityId。 ActivityId 将与任何其他跟踪信息一起写入日志,从而可以挑选出单个“活动”的跟踪信息。这对于 WCF 来说非常有用,因为 ActivityId 可以转移到服务组件。
下面是我正在讨论的一个示例:
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
{
DoWork();
}));
}
static void DoWork()
{
try
{
Trace.CorrelationManager.ActivityId = Guid.NewGuid();
//The functions below contain tracing which logs the ActivityID.
CallFunction1();
CallFunction2();
CallFunction3();
}
catch (Exception ex)
{
Trace.Write(Trace.CorrelationManager.ActivityId + " " + ex.ToString());
}
}
现在,对于 TPL,我的理解是多个任务共享线程。这是否意味着 ActivityId 很容易在任务中(由另一个任务)重新初始化?是否有新的机制来处理活动跟踪?
Before using the Task Parallel Library, I have often used CorrelationManager.ActivityId to keep track of tracing/error reporting with multiple threads.
ActivityId is stored in Thread Local Storage, so each thread get's its own copy. The idea is that when you fire up a thread (activity), you assign a new ActivityId. The ActivityId will be written to the logs with any other trace information, making it possible to single out the trace information for a single 'Activity'. This is really useful with WCF as the ActivityId can be carried over to the service component.
Here is an example of what I'm talking about:
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
{
DoWork();
}));
}
static void DoWork()
{
try
{
Trace.CorrelationManager.ActivityId = Guid.NewGuid();
//The functions below contain tracing which logs the ActivityID.
CallFunction1();
CallFunction2();
CallFunction3();
}
catch (Exception ex)
{
Trace.Write(Trace.CorrelationManager.ActivityId + " " + ex.ToString());
}
}
Now, with the TPL, my understanding is that multiple Tasks share Threads. Does this mean that ActivityId is prone to being reinitialized mid-task (by another task)? Is there a new mechanism to deal with activity tracing?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我进行了一些实验,结果证明我的问题中的假设是不正确的 - 使用 TPL 创建的多个任务不会同时在同一线程上运行。
ThreadLocalStorage 可以安全地与 .NET 4.0 中的 TPL 一起使用,因为一个线程一次只能由一个任务使用。
任务可以同时共享线程的假设是基于我听说的一次采访< strong>c# 5.0 on DotNetRocks (抱歉,我不记得那是哪个节目了) - 所以我的问题可能(或可能不会)很快变得相关。
我的实验启动了多个任务,并记录了运行了多少个任务、运行了多长时间以及消耗了多少个线程。如果有人想重复的话,代码如下。
输出(当然这取决于机器)是:
将 taskCreationOpt 更改为 TaskCreationOptions.LongRunning 给出了不同的结果:
I ran some experiments and it turns out the assumption in my question is incorrect - multiple tasks created with the TPL do not run on the same thread at the same time.
ThreadLocalStorage is safe to use with TPL in .NET 4.0, since a thread can only be used by one task at a time.
The assumption that tasks can share threads concurrently was based on an interview I heard about c# 5.0 on DotNetRocks (sorry, I can't remember which show it was) - so my question may (or may not) become relevant soon.
My experiment starts a number of tasks, and records how many tasks ran, how long they took, and how many threads were consumed. The code is below if anyone would like to repeat it.
The output (of course this will depend on the machine) was:
Changing taskCreationOpt to TaskCreationOptions.LongRunning gave different results:
请原谅我将其发布为答案,因为它并不是真正回答您的问题,但是,它与您的问题相关,因为它涉及 CorrelationManager 行为和线程/任务/等。我一直在考虑使用 CorrelationManager 的 LogicalOperationStack(和 StartLogicalOperation/StopLogicalOperation 方法)在多线程场景中提供额外的上下文。
我采用了您的示例并对其进行了稍微修改,以添加使用 Parallel.For 并行执行工作的能力。另外,我使用
StartLogicalOperation/StopLogicalOperation
来括起(内部)DoLongRunningWork
。从概念上讲,DoLongRunningWork 每次执行时都会执行类似的操作:我发现,如果我将这些逻辑操作添加到代码中(或多或少按原样),所有逻辑操作都会保持同步(堆栈上的操作数始终是预期的,并且堆栈上操作的值始终符合预期)。
在我自己的一些测试中,我发现情况并非总是如此。逻辑操作堆栈正在“损坏”。我能想到的最好的解释是,当“子”线程退出时,将 CallContext 信息“合并”回“父”线程上下文会导致“旧”子线程上下文信息(逻辑操作)变为“由另一个兄弟子线程继承”。
该问题也可能与以下事实有关:Parallel.For 显然使用主线程(至少在示例代码中,如所写)作为“工作线程”之一(或在并行域中应调用它们的任何线程)。每当执行 DoLongRunningWork 时,都会启动一个新的逻辑操作(在开始时)并停止(在结束时)(即,推入 LogicalOperationStack 并从其中弹出)。如果主线程已经有一个有效的逻辑操作,并且 DoLongRunningWork 在主线程上执行,则将启动一个新的逻辑操作,因此主线程的 LogicalOperationStack 现在有两个操作。 DoLongRunningWork 的任何后续执行(只要 DoLongRunningWork 的这一“迭代”在主线程上执行)将(显然)继承主线程的 LogicalOperationStack(现在它有两个操作,而不仅仅是一个预期操作)。
我花了很长时间才弄清楚为什么 LogicalOperationStack 的行为在我的示例中与我的示例的修改版本中不同。最后我发现在我的代码中我已经将整个程序括在逻辑运算中,而在我的测试程序的修改版本中我没有。这意味着在我的测试程序中,每次执行我的“工作”(类似于DoLongRunningWork)时,已经有一个有效的逻辑操作。在我对测试程序的修改版本中,我没有将整个程序括在逻辑运算中。
因此,当我修改您的测试程序以将整个程序括在逻辑运算中并且如果我使用 Parallel.For 时,我遇到了完全相同的问题。
使用上面的概念模型,这将成功运行:
虽然这最终会由于明显不同步的 LogicalOperationStack 而断言:
这是我的示例程序。它与您的类似,因为它有一个操作 ActivityId 和 LogicalOperationStack 的 DoLongRunningWork 方法。我对 DoLongRunningWork 的踢法也有两种风格。一种使用任务,一种使用 Parallel.For。还可以执行每种风格,使得整个并行操作包含在逻辑操作中或不包含在逻辑操作中。因此,共有4种并行操作的执行方式。要尝试每一种方法,只需取消注释所需的“Use...”方法,重新编译并运行即可。
UseTasks
、UseTasks(true)
和UseParallelFor
应该全部运行完成。UseParallelFor(true)
将在某个时刻断言,因为 LogicalOperationStack 没有预期的条目数。LogicalOperationStack 是否可以与 Parallel.For (和/或其他线程/任务构造)一起使用或如何使用它的整个问题可能值得它自己的问题。也许我会发布一个问题。同时,我想知道您对此是否有任何想法(或者,我想知道您是否考虑过使用 LogicalOperationStack,因为 ActivityId 似乎是安全的)。
[编辑]
请参阅我对此的回答 有关使用 LogicalOperationStack 和/或 CallContext.LogicalSetData 以及各种 Thread/ThreadPool/Task/Parallel 构造中的一些的更多信息,请参阅问题。
另请参阅我关于 LogicalOperationStack 和 Parallel 扩展的问题:
CorrelationManager.LogicalOperationStack 是否与 Parallel.For、任务、线程等兼容< /a>
最后,另请参阅我在 Microsoft 并行扩展论坛上的问题:
http://social. msdn.microsoft.com/Forums/en-US/parallelextensions/thread/7c5c3051-133b-4814-9db0-fc0039b4f9d9
在我的测试中,使用 Parallel.For 或 Parallel 时,Trace.CorrelationManager.LogicalOperationStack 可能会损坏.如果您在主线程中启动逻辑操作,然后在委托中启动/停止逻辑操作,则调用。在我的测试中(请参阅上面的两个链接之一),当执行 DoLongRunningWork 时,LogicalOperationStack 应该始终有 2 个条目(如果我在使用各种技术启动 DoLongRunningWork 之前在主线程中启动逻辑操作)。因此,我所说的“损坏”是指 LogicalOperationStack 最终将拥有超过 2 个条目。
据我所知,这可能是因为 Parallel.For 和 Parallel.Invoke 使用主线程作为“工作”线程之一来执行 DoLongRunningWork 操作。
使用存储在 CallContext.LogicalSetData 中的堆栈来模拟 LogicalOperationStack 的行为(类似于通过 CallContext.SetData 存储的 log4net 的 LogicalThreadContext.Stacks)会产生更糟糕的结果。如果我使用这样的堆栈来维护上下文,那么在几乎所有主线程中有“逻辑操作”并且每次迭代中有逻辑操作的情况下,它都会被损坏(即没有预期的条目数) /DoLongRunningWork 委托的执行。
Please forgive my posting this as an answer as it is not really answer to your question, however, it is related to your question since it deals with CorrelationManager behavior and threads/tasks/etc. I have been looking at using the CorrelationManager's
LogicalOperationStack
(andStartLogicalOperation/StopLogicalOperation
methods) to provide additional context in multithreading scenarios.I took your example and modified it slightly to add the ability to perform work in parallel using Parallel.For. Also, I use
StartLogicalOperation/StopLogicalOperation
to bracket (internally)DoLongRunningWork
. Conceptually,DoLongRunningWork
does something like this each time it is executed:I have found that if I add these logical operations to your code (more or less as is), all of the logical operatins remain in sync (always the expected number of operations on stack and the values of the operations on the stack are always as expected).
In some of my own testing I found that this was not always the case. The logical operation stack was getting "corrupted". The best explanation I could come up with is that the "merging" back of the CallContext information into the "parent" thread context when the "child" thread exits was causing the "old" child thread context information (logical operation) to be "inherited" by another sibling child thread.
The problem might also be related to the fact that Parallel.For apparently uses the main thread (at least in the example code, as written) as one of the "worker threads" (or whatever they should be called in the parallel domain). Whenever DoLongRunningWork is executed, a new logical operation is started (at the beginning) and stopped (at the end) (that is, pushed onto the LogicalOperationStack and popped back off of it). If the main thread already has a logical operation in effect and if DoLongRunningWork executes ON THE MAIN THREAD, then a new logical operation is started so the main thread's LogicalOperationStack now has TWO operations. Any subsequent executions of DoLongRunningWork (as long as this "iteration" of DoLongRunningWork is executing on the main thread) will (apparently) inherit the main thread's LogicalOperationStack (which now has two operations on it, rather than just the one expected operation).
It took me a long time to figure out why the behavior of the LogicalOperationStack was different in my example than in my modified version of your example. Finally I saw that in my code I had bracketed the entire program in a logical operation, whereas in my modified version of your test program I did not. The implication is that in my test program, each time my "work" was performed (analogous to DoLongRunningWork), there was already a logical operation in effect. In my modified version of your test program, I had not bracketed the entire program in a logical operation.
So, when I modified your test program to bracket the entire program in a logical operation AND if I am using Parallel.For, I ran into exactly the same problem.
Using the conceptual model above, this will run successfully:
While this will eventually assert due to an apparently out of sync LogicalOperationStack:
Here is my sample program. It is similar to yours in that it has a DoLongRunningWork method that manipulates the ActivityId as well as the LogicalOperationStack. I also have two flavors of kicking of DoLongRunningWork. One flavor uses Tasks one uses Parallel.For. Each flavor can also be executed such that the whole parallelized operation is enclosed in a logical operation or not. So, there are a total of 4 ways to execute the parallel operation. To try each one, simply uncomment the desired "Use..." method, recompile, and run.
UseTasks
,UseTasks(true)
, andUseParallelFor
should all run to completion.UseParallelFor(true)
will assert at some point because the LogicalOperationStack does not have the expected number of entries.This whole issue of if LogicalOperationStack can be used with Parallel.For (and/or other threading/Task constructs) or how it can be used probably merits its own question. Maybe I will post a question. In the meantime, I wonder if you have any thoughts on this (or, I wonder if you had considered using LogicalOperationStack since ActivityId appears to be safe).
[EDIT]
See my answer to this question for more information about using LogicalOperationStack and/or CallContext.LogicalSetData with some of the various Thread/ThreadPool/Task/Parallel contstructs.
See also my question here on SO about LogicalOperationStack and Parallel extensions:
Is CorrelationManager.LogicalOperationStack compatible with Parallel.For, Tasks, Threads, etc
Finally, see also my question here on Microsoft's Parallel Extensions forum:
http://social.msdn.microsoft.com/Forums/en-US/parallelextensions/thread/7c5c3051-133b-4814-9db0-fc0039b4f9d9
In my testing it looks like Trace.CorrelationManager.LogicalOperationStack can become corrupted when using Parallel.For or Parallel.Invoke IF you start a logical operation in the main thread and then start/stop logical operations in the delegate. In my tests (see either of the two links above) the LogicalOperationStack should always have exactly 2 entries when DoLongRunningWork is executing (if I start a logical operation in the main thread before kicking of DoLongRunningWork using various techniques). So, by "corrupted" I mean that the LogicalOperationStack will eventually have many more than 2 entries.
From what I can tell, this is probably because Parallel.For and Parallel.Invoke use the main thread as one of the "worker" threads to perform the DoLongRunningWork action.
Using a stack stored in CallContext.LogicalSetData to mimic the behavior of the LogicalOperationStack (similar to log4net's LogicalThreadContext.Stacks which is stored via CallContext.SetData) yields even worse results. If I am using such a stack to maintain context, it becomes corrupted (i.e. does not have the expected number of entries) in almost all of the scenarios where I have a "logical operation" in the main thread and a logical operation in each iteration/execution of the DoLongRunningWork delegate.