Which approach to take with this multithreading problem?
Short question:
I would like to spawn a single background thread that would process work items submitted to a queue (like a threadpool with one thread). Some of the work items are capable of reporting progress, some are not. Which one of the .NET's myriad 开发者_JAVA百科of multithreading approaches should I use?
Long explanation (to avoid asking about the half which doesn't make any sense):
The main window of my winforms application is split vertically into two halves. The left half contains a treeview with items. When the user double-clicks an item in the treeview, the item is opened on the right half. Almost all objects have a lot of properties, split into several sections (represented by tabs). The loading of these properties takes quite a lot of time, typically around 10s, sometimes more. And more properties are added every once in a while, so the time increases.
Currently my single-threaded design makes the UI non-responsive for this time. Naturally this is undesirable. I'd like to load things part-by-part in background and as soon as a part is loaded make it available for use. For other parts I would display a placeholder tab with a loading animation or something. Also, while some parts are loaded in a single lengthy monolithic operation, others consist of lots of smaller function calls and calculations, and could thus display loading progress. For these parts it would be nice to see the progress (especially if they hang somewhere, which happens).
Note that the data source is not thread-safe, so I cannot load two parts simultaneously.
What approach would be best to implement this behavior? Is there some .NET class that would lift some work off my shoulders, or should I just get down and dirty with Thread
?
A ThreadPool
does work item queue management, but there are no facilities for progress reporting. BackgroundWorker
on the other hand supports progress reports, but it's meant for a single work item. Is there perhaps a combination of both?
.NET 4.0 brings a lot of improvements to multithreading by introducing the Task
type, which represents a single possibly-asynchronous operation.
For your scenario, I'd recommend splitting up the loading of each property (or property group) into separate tasks. Tasks include the notion of a "parent", so the loading of each object could be a parent task owning the property-loading tasks.
To handle cancellation, use the new unified cancellation framework. Create a CancellationTokenSource
for each object and pass its CancellationToken
to the parent task (which passes it to each of its child tasks). This allows an object to be cancelled, which can take effect after the currently-loading property is done (instead of waiting until the whole object is done).
To handle concurrency (or more properly, non-concurrency), use the OrderedTaskScheduler
from the ParallelExtensionsExtras sample library. Each Task
only represents a unit of work that needs to be scheduled, and by using OrderedTaskScheduler
, you ensure sequential execution (on a ThreadPool thread).
UI progress updates can be done by creating a UI update Task
and scheduling it to the UI thread. I have an example of this on my blog, where I wrap some of the more awkward methods into a ProgressReporter
helper type.
One nice thing about the Task
type is that it propogates exceptions and cancellation in a natural manner; those are often the more difficult parts of designing a system to handle a problem like yours.
Use a thread, drop your work in a thread safe collection and use invoke when you update your ui to do it in the right thread
Sounds tricky!
You say your data source is not thread-safe. So, what does this mean for the user. If they're clicking around all over the place, but don't wait for properties to load up before clicking somewhere else, they could click on 10 nodes which take a long time to load, and then sit waiting on the 10th one. The load's have to run one after the other as the data source access is not thread safe. This indicates a ThreadPool wouldn't be a good choice as it would run loads in parallel and break the thread safety. It would be good if a load could be aborted part way through to prevent the user having to wait for the last 9 nodes to load up before the page they want to see starts loading.
If loads can be aborted, I'd suggest a BackgroundWorker would be best. If the user switches node, and the BackgroundWorker is already busy, set an event or something to signal it should abort existing work, and then queue up the new work to load the current page.
Also, consider, it isn't too tricky to make a thread running in a thread pool report progress. To do this pass a progress object to the QueueUserWorkItem call of a type something like this:
class Progress
{
object _lock = new Object();
int _current;
bool _abort;
public int Current
{
get { lock(_lock) { return _current; } }
set { lock(_lock) { _current = value; } }
}
public bool Abort
{
get { lock(_lock) { return _abort; } }
set { lock(_lock) { _abort = value; } }
}
}
The thread can write to this, and the ui thread can Poll (from a System.Windows.Forms.Timer event) to read the progress and update a progress bar or animation.
Also, if you include an Abort property. The ui can set it if the user changes node. The load method can at various points throughout its operation check the abort value, and if it's set, return without completing the load.
To be quite honest, which you choose doesn't fantastically matter. All three options get stuff done on a background thread. If I were you, I'd get started with the BackgroundWorker as it's pretty simple to setup, and if you decide you need something more, consider switching to the ThreadPool or plain Thread afterwards.
The BackgroundWorker also has the advantage that you can use it's completed event (which is executed on the main ui thread) to update the ui with the data that was loaded.
精彩评论