Task Parallel Library Recycling in-use Threads
Our system writes the ManagedThreadID with every message written and for years we've used that to help distinguish a particular unit of work among many in the logs. So far, so good.
Now, we're beginning to use the Task Parallel Library and noticing an interesting effect:
public static void Main(string[] args) {
WriteLine("BEGIN");
Parallel.For(0, 32, (index) => {
WriteLine(" Loop " + index.ToString());
});
WriteLine("END");
}
The output looks something like:
ThreadID=1, Message=BEGIN
ThreadID=1, Message= Loop 0
ThreadID=3, Message= Loop 16
ThreadID=3, Message= Loop 17
...
ThreadID=4, Message= Loop 4
ThreadID=4, Message= Loop 5
ThreadID=1, Message= Loop 8
ThreadID=1, Message= Loop 9
ThreadID=1, Message= Loop 10
ThreadID=3, Message= Loop 21
ThreadID=4, Message= Loop 6
...
ThreadID=3, Message= Loop 24
ThreadID=3, Message= Loop 25
ThreadID=1, Message= Loop 11
ThreadID=1, Message= Loop 12
ThreadID=1, Message= Loop 13
ThreadID=1, Message= Loop 31
ThreadID=3, Message= Loop 26
...
ThreadID=3, Message= Loop 30
ThreadID=1, Message=END
You'll notice that the ThreadID of the main thread (marked "BEGIN") is recycled in the Loop threads on occasion.
My question is: can this happen anywhere else -- such as thread pool or when using other features of the Task Parallel Library? I have spent a rediculous amount of time trying to figure out other ways to provoke the behavior and cannot.
The concern here is that if we cannot rely on the ThreadID anymore (we have many tools to rely on this behavior) then we will just avoid using Parallel.For. But if the problem will manifest in other ways, we'll need to figure out how to avoid them UNTIL we revamp our logging strategy and tooling support.
If there are other ways to provoke the behavior, I'd like to know about it so I can determine if any of our usage meets such conditions so we can correct it accordingly. More imporantly, so I can get a sample program to pr开发者_C百科ovoke the behavior and study any side-effects in our tooling.
Parallel.For indeed runs one of the worker tasks on the calling thread. The rationale is that since the calling thread must wait until the parallel loop completes, it might as well participate in the parallel operation.
As far as other features of Task Parallel Library go, methods that are blocking will often use the calling thread. So, Parallel.For, Parallel.ForEach, Parallel.Invoke and blocking PLINQ queries will all reuse the calling thread as one of the workers. On the other hand, operations that just "kick-off" some work and immediately return - like Task.Factory.StartNew, Threadpool.QueueUserWorkItem, and non-blocking PLINQ queries - cannot use the calling thread.
As a workaround, you can run the Parallel.For inside a task and wait on the task:
public static void Main(string[] args) {
WriteLine("BEGIN");
Task.Factory.StartNew(() =>
Parallel.For(0, 32, (index) => {
WriteLine(" Loop " + index.ToString());
})
).Wait();
WriteLine("END");
}
Warning: the workaround above will not work if Task.Factory.StartNew() is called from a ThreadPool thread. In that case, the Wait call may end up executing the task inline on the calling ThreadPool thread.
You would have to not use TPL to avoid that behavior
It sounds like you need to find a different way of identifying a unit of work besides the ThreadID, possibly using the logical call context to pass along information about the current thread if you cant rewrite code to pass along some kind of identifier.
精彩评论