I have a program that uses threads to perform time-consuming processes sequentially. I want to be able to monitor the progress of each thread similar to the way that the BackgroundWorker.ReportProgress
/ProgressChanged
model does. I can't use ThreadPool
or BackgroundWorker
due to other constraints I'm under. What is the best way to allow/expose this functionality. Overload the Thread
class and add a property/event? Another more-elegant solution?
views:
150answers:
4Overload the Thread class and add a property/event?
If by "overload" you actually mean inherit then no. The Thread
is sealed so it cannot be inherited which means you will not be able to add any properties or events to it.
Another more-elegant solution?
Create a class that encapsulates the logic that will be executed by the thread. Add a property or event (or both) which can be used to obtain progress information from it.
public class Worker
{
private Thread m_Thread = new Thread(Run);
public event EventHandle<ProgressEventArgs> Progress;
public void Start()
{
m_Thread.Start();
}
private void Run()
{
while (true)
{
// Do some work.
OnProgress(new ProgressEventArgs(...));
// Do some work.
}
}
private void OnProgress(ProgressEventArgs args)
{
// Get a copy of the multicast delegate so that we can do the
// null check and invocation safely. This works because delegates are
// immutable. Remember to create a memory barrier so that a fresh read
// of the delegate occurs everytime.
Thread.MemoryBarrier();
var local = Progress;
Thread.MemoryBarrier();
if (local != null)
{
local(this, args);
}
}
}
Update:
Let me be a little more clear on why a memory barrier is necessary in this situation. The barrier prevents the read from being moved before other instructions. The most likely optimization is not from the CPU, but from the JIT compiler "lifting" the read of Progress
outside of the while
loop. This movement gives the impression of "stale" reads. Here is a semi-realistic demonstration of the problem.
class Program
{
static event EventHandler Progress;
static void Main(string[] args)
{
var thread = new Thread(
() =>
{
var local = GetEvent();
while (local == null)
{
local = GetEvent();
}
});
thread.Start();
Thread.Sleep(1000);
Progress += (s, a) => { Console.WriteLine("Progress"); };
thread.Join();
Console.WriteLine("Stopped");
Console.ReadLine();
}
static EventHandler GetEvent()
{
//Thread.MemoryBarrier();
var local = Progress;
return local;
}
}
It is imperative that a Release build is ran without the vshost process. Either one will disable the optimization that manifest the bug (I believe this is not reproducable in framework version 1.0 and 1.1 as well due to their more primitive optimizations). The bug is that "Stopped" is never displayed even though it clearly should be. Now, uncomment the call to Thread.MemoryBarrier
and notice the change in behavior. Also keep in mind that even the most subtle changes to the structure of this code currently inhibit the compiler's ability to make the optimization in question. One such change would be to actually invoke the delegate. In other words you cannot currently reproduce the stale read problem using the null check followed by an invocation pattern, but there is nothing in the CLI specification (that I am aware of anyway) that prohibits a future hypothetical JIT compiler from reapplying that "lifting" optimization.
I tried this some time ago and it worked for me.
- Create a
List
-like class with locks. - Have your threads add data to an instance of the class you created.
- Place a timer in your Form or wherever you want to record the log/progress.
- Write code in the
Timer.Tick
event to read the messages the threads output.
You might also want to check out the Event-based Asynchronous Pattern.
Provide each thread with a callback that returns a status object. You can use the thread's ManagedThreadId
to keep track of separate threads, such as using it as a key to a Dictionary<int, object>
. You can invoke the callback from numerous places in the thread's processing loop or call it from a timer fired from within the thread.
You can also use the return argument on a callback to signal the thread to pause or halt.
I've used callbacks with great success.