I am attempting to make my GUI thread remain responsive-ish during long running operations. These operations must be synchronous as they are usually operations which are required to finish before the requested operation can complete.
I was attempting to do this with a background worker, monitors and a lock object. Essentially I want to start a timer before the long running process is started, start the long running process in the background thread and wait on the background worker thread to signify that it has finished before continuing with the dependant code. If the long running process takes too long then show a 'loading...' dialogue to the user so they are aware the app has not crashed.
An example of this might be User clicks button in graphics package, loading of a large image from disk must happen before we can draw the image then print pi calculated to a million decimal places over the top.
I cannot make the loading of the image from disk asynchronous which would keep the UI responsive as the user could initiate another operation which messes up the program state (i.e. an undo operation).
I could simply change the cursor to be an hourglass and be done with it, but in a number of instances I want the user to be able to cancel the operation too - a "Loading..." dialogue with a cancel button would take care of this quite well.
What I was aiming for originally was using a lock object and System.Threading.Monitor.Ent开发者_开发百科er()
so that the UI thread waits until the long running thread has finished, then it continues executing. If the timer fires before the long running thread has completed then the UI thread is still available to handle the event and draw the dialogue box on the screen.
The issue I am encountering is that I cannot get the Background worker to lock the object before the UI thread attempts to get a lock.
Rather annoyingly I am using some 3rd party code which is very black box to do processing. Hence I can't tailor the code to be thread friendly and report its progress or support cancelling.
My Question
Is there any proven way wrapping 3rd party code so that the UI thread remains responsive and I can show a cancel dialogue if need be? - There will be a number of instances where the long running operation completes almost instantly and doesn't require a dialog to be shown.
A little futher clarification
Why do I want to do this? Asynchronous operations are the darling of windows applications...
Well I don't want to have to lock down every aspect of the user interface when a long running async operation is started, then unlock every aspect when it has finished. I could - either by setting the cursor or physically disabling all the buttions etc etc, but realistically I'd prefer to be able to simply wrap the call in 'some object/method etc' which will allow a dialog to popup up if (and only if) the operation is taking long enough to impact on the user. I'd not have to worry about changes to the flow of execution, I'd still (on the whole) be able to maintain atomic operations in code (not split across callbacks) and still have a 'responsive' UI.
I can understand why I've been unsuccessful so far in crafting a BackgroundWorker / Thread into a synchronous blocking thread, but I do worry that I'm going to have to go down the while(true){ sleep() }
route in the GUI thread rather than using locks.
Before you get any further I would seriously consider the BackgroundWorker class in .NET. You said you used a "background worker", so I am not sure if that is what you meant. It has facilities for calling back to your UI with progress notifications from within your worker function. With the progress notifications, it should significantly cut down on your need for synchronization objects.
This is how I'd do that:
Code to start long operation:
private void button1_Click(object sender, EventArgs e)
{
Thread thr = new Thread(LongMethod);
thr.Start();
// wait for 250 ms. If the thread is not finished, we show a from in a modal way
// saying something like "please wait for the operation to complete"
if(!thr.Join(250))
{
pleaseWaitForm.Thread = thr;
pleaseWaitForm.ShowDialog();
}
}
Then, in the "Please wait" form
public Thread Thread {get;set;}
private void PleasWait_FormClosing(object sender, FormClosingEventArgs e)
{
// do not allow closing of the form while thread is running
if (this.Thread.IsAlive)
e.Cancel = true;
}
public void JoinAndClose(){
// this is a method that allows closing by waiting for the thread to finish
this.Thread.Join();
Close();
}
And finally, in the long method:
private void LongMethod(){
// Do some stuff
Threading.Thread.Sleep(100000);
// Actually close the "please wait" window
pleaseWaitForm.Invoke(new Action(()=>pleaseWaitForm.JoinAndClose()))
}
This is a bit sketchy and probably has some bugs, but the general idea is simple - do a timed Join()
to only show dialog if the operation is long, and close the dialog from the long-running thread itself.
I would put your worker in a separate class and run it on it's own thread, and use callbacks to signal your UI of progress and completion. The UI can use the callback to update a progress bar and enable other controls when the worker thread completes. Callbacks can also return values so you can use it to halt the worker cleanly.
Here's a very simple example:
public delegate void CallbackDelegate(string messageArg);
class Program
{
static void Main(string[] args)
{
Worker worker = new Worker();
worker.Callback = new CallbackDelegate(WorkerStatus);
Thread thread = new Thread(new ThreadStart(worker.DoSomething));
thread.IsBackground = true;
thread.Start();
Console.ReadLine(); // wait for enter key
}
static void WorkerStatus(string statusArg)
{
Console.WriteLine(statusArg);
}
}
public class Worker
{
public CallbackDelegate Callback { private get; set; }
public void DoSomething()
{
for (int i = 0; i < 10; i++)
{
Callback(i.ToString());
}
Callback("done");
}
}
I am assuming this is windows forms? A cheapish way to get responsiveness back while actually blocking is a pattern like this:
// Method call would be foo.DoWork()
Action f = foo.DoWork;
var asyncresult = f.BeginInvoke(null, null);
while (!asyncresult.IsCompleted)
Application.DoEvents();
In this case the user can still click a button to cancel the operation. Please note that it is generally not a good idea to abort a thread - It wreaks havoc with any state your program may be operating. A better idea is to to regularly check whether some bool cancel field has been set to true and then gracefully end the operation at the next possible moment.
精彩评论