How do I pass information from a ThreadPool.QueueUserWorkItem back to the UI thread?
I have a rather simple threading question.
I'm writing a simple utility that will run various SQL scripts based on parameters defined by the user.
In order to keep the UI responsive and provide feedback as to the status of the scripts that are being executed, I've decided that using ThreadPool.QueueUserWorkItem
would be appropriate to handle the execution of the various scripts (via SMO.)
However, I'm a bit confused as to how I can relay the output information that SMO will开发者_Go百科 return back to the UI thread.
For this utility, I'm using WPF and MVVM for the presentation. I'm thinking that I would have a ScriptWorker
class that I could pass the parameters and locations and order in which to run the scripts to.
After I run each script, I'd like to somehow return the results to the UI thread so that it updates the output window and then I'd like for the worker to move to the next task.
I'm certain this is a basic question, but after looking at QueueUserWorkItem
and seeing that I essentially start the work through a callback, I'm unsure how I'd accomplish what I'd like to accomplish.
I'm basing my assumptions off of this Microsoft article:
http://msdn.microsoft.com/en-us/library/3dasc8as(VS.80).aspx
Thanks for the info!
QueueUserWorkItem
would technically work, but is extremely low-level. There are easier ways.
I recommend using the new Task
feature of .NET 4.0. It does exactly what you want, including synchronizing the result or error conditions to another thread (the UI thread, in this case).
If .NET 4.0 is not an option, then I'd recommend either BackgroundWorker
(if your background processing is not too complex), or asynchronous delegates such as Hans mentioned. If you use async delegates, then use the AsyncOperation
class to marshal the results back to the UI thread.
The Task
option is very nice because it handles parent/child tasks very naturally. BackgroundWorker
can't be nested. Another consideration is cancellation; Task
and BackgroundWorker
have built-in support for cancellation, but for async delegates you'd have to do your own.
The only place where Task
is a bit more complex than BackgroundWorker
is in progress reporting. It's not quite as easy as BackgroundWorker
, but I have a wrapper on my blog to minimize that.
To summarize, in order of preference:
Task
- supports proper marshaling of errors, the concept of a result, cancellation, and parent/child nesting. Its one weakness is that progress reporting isn't simple (you have to create another Task and schedule it to the UI thread).BackgroundWorker
- supports proper marshaling of errors, the concept of a result, cancellation, and progress reporting. Its one weakness is that it doesn't support parent/child nesting, and that limits its usage in APIs, e.g., for a business layer.Delegate.BeginInvoke
withAsyncOperation
- supports proper marshaling of erros, the concept of a result, and progress reporting. However, there is not a built-in concept of cancellation (though it can be done by hand using avolatile bool
). It also does not support parent/child nesting.Delegate.BeginInvoke
withSynchronizationContext
- this is the same as option (3) except it usesSynchronizationContext
directly. The code is slightly more complex, but the tradeoff is that parent/child nesting is supported. All other limitations are identical to option (3).ThreadPool.QueueUserWorkItem
withAsyncOperation
orSynchronizationContext
- supports the concept of progress reporting. Cancellation suffers from the same problem as option (3). Marshaling of errors is not easy (in particular, preserving the stack trace). Also, parent/child nesting is only possible if theSynchronizationContext
is used instead ofAsyncOperation
. Furthermore, this option does not support the concept of a result, so any return value(s) need to be passed as arguments.
As you can see, Task
is the clear winner. It should be used unless .NET 4.0 is not an option.
This article has a simple example of what you want.
To get back to the UI thread you need a reference to the ISynchronizeInvoke
interface. The Form
class for example implements this interface.
In pseudocode you could do something like this:
public class MyForm : Form
{
private OutputControl outputControl;
public void btnClick(...)
{
// Start a long running process that gives feedback to UI.
var process = new LongRunningProcess(this, outputControl);
ThreadPool.QueueUserWorkItem(process.DoWork);
}
}
class LongRunningProcess
{
// Needs a reference to the interface that marshals calls back to the UI
// thread and some control that needs updating.
public LongRunningProcess(ISynchonizeInvoke invoker,
OutputControl outputControl)
{
this.invoker = invoker;
this.outputControl = outputControl;
}
public void DoWork(object state)
{
// Do long-running job and report progress.
invoker.Invoke(outputControl.Update(...));
}
}
Note that the OutputControl
in this example is a control and therefore also implements the ISynchronizeInvoke
interface so you can also choose to call Invoke
directly on this control.
The approach sketched above is rather low-level but gives you a lot of control, especially over how you want to report progress. BackgroundWorker
gives you a more high-level solution but less control. You can only provide progress state via the untyped UserState
property.
精彩评论