How to use multithreading with Winform?
I'm a newbie with multithreading. I have a winform that have a label and a progress bar.
I wanna show the processing result. Firstly, I use Application.DoEvents()
method. But, I find that the form is freezing.
Then I read some article about multithreading at MSDN.
Secondly, I use a BackgroundWorker to do it.
this.bwForm.DoWork += (o, arg) => { DoConvert(); };
this.bwForm.RunWorkerAsync();
The form doesn't freeze that I can drag/drog when processing. Unfortunately, it throws an InvalidOperationException. So I have to use this. Control.CheckForIllegalCrossThreadCalls = false;
I ensure that's not a final solution.
Do you have some suggestion to do so, Experts?
Edit: When I call listview throw InvalidOperationException. This code is in DoWork().
foreach (ListViewItem item in this.listView1.Items)
{
开发者_JAVA技巧 //........some operation
lbFilesCount.Text = string.Format("{0} files", listView1.Items.Count);
progressBar1.Value++;
}
Edit2: I use delegate and invoke in DoWork() and it don't throw exeption. But the form is freezing again. How to do it so the form could be droppable/drappable while processing?
You can only set progress bar user control properties from the UI thread (the WinForm one). The easiest way to do it with your BackgroundWorker is to use the ProgressChanged event:
private BackgroundWorker bwForm;
private ProgressBar progressBar;
In WinForm constructor:
this.progressBar = new ProgressBar();
this.progressBar.Maximum = 100;
this.bwForm = new BackgroundWorker();
this.bwForm.DoWork += new DoWorkEventHandler(this.BwForm_DoWork);
this.bwForm.ProgressChanged += new ProgressChangedEventHandler(this.BwForm_ProgressChanged);
this.bwForm.RunWorkerAsync();
...
void BwForm_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bgw = sender as BackgroundWorker;
// Your DoConvert code here
// ...
int percent = 0;
bgw.ReportProgress(percent);
// ...
}
void BwForm_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.progressBar.Value = e.ProgressPercentage;
}
see here: http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx
EDIT
So I didn't understand your question, I thought it was about showing progress bar progression during work. If it's about result of work use e.Result in the BwForm_DoWork event. Add a new event handler for completed event and manage result:
this.bwForm.RunWorkerCompleted += new RunWorkerCompletedEventHandler(this.BwForm_RunWorkerCompleted);
...
private void BwForm_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
YourResultStruct result = e.Result as YourResultStruct;
if (e.Error != null && result != null)
{
// Handle result here
}
}
Invoke the UI thread. For example:
void SetControlText(Control control, string text)
{
if (control.InvokeRequired)
control.Invoke(SetControlText(control, text));
else
control.Text = text;
}
Avoid calling Application.DoEvents
. Often times this leads to more problems than it solves. It is generally considered bad practice because there exists better alternatives which keep the UI pumping messages.
Avoid changing setting CheckForIllegalCrossThreadCalls = false
. This does not fix anything. It only masks the problem. The problem is that you are attempting to access a UI element from a thread other than the main UI thread. If you disable CheckForIllegalCrossThreadCalls
then you will no longer get the exception, but instead your application will fail unpredictably and spectacularly.
From within the DoWork
event handler you will want to periodically call ReportProgress
. From your Form
you will want to subscribe to the ProgressChanged
event. It will be safe to access UI elements from within the ProgressChanged
event handler because it is automatically marshaled onto the UI thread.
You should use Control.Invoke(). Also see this question for details.
Cleanest way is using BackGroundWorker
as you did.
you just missed some points:
- you cannot access your form elements in
DoWork
event handler because that makes a cross thread method invokation, this should be done inProgressChanged
event handler. - by default a
BackGroundWorker
will not allow to report progress, as well as in won't allow Cancelling the operation. When you add aBackGroundWorker
to your code, you have to set theWorkerReportsProgress
property of thatBackGroundWorker
totrue
if you want to call theReportProgress
method of yourBackGroundWorker
. - In case you need to allow the user to cancel the operation as well, set
WorkerSupportsCancellation
to true and in your loop in theDoWork
event handler check the property namedCancellationPending
in yourBackGroundWorker
I hope I helped
There are some fundamental problems with the code you have shown. As other have mentioned, Application.DoEvents()
will not do what you want from a background thread. Separate your slow background processing into a BackgroundWorker
and update your progress bar in the UI thread. You are calling progressBar1.Value++
from a background thread, which is wrong.
Never call Control.CheckForIllegalCrossThreadCalls = false
, that will only hide errors.
If you want to update a progress bar from a background thread, you will have to implement a ProgressChanged
handler; you are not doing that.
If you need an example of implementing a BackgroundWorker
, consult the article BackgroundWorker Threads from Code Project.
I would say writing thread safe applications is one of the more difficult things to understand and do properly. You really need to read up on it. If you learned C# from a book go back and see if there isn't a chapter on Multithreading. I learned from Andrew Troelsen's books (the latest was Pro C# 2005 and the .NET 2.0 Platform), he doesn't touch this topic until Chapter 14 (so a lot of people have quit reading by then). I come from an embedded programming background where concurrency and atomicity are also concerns so this is not a .NET or Windows specific issue.
A lot of those posts here are detailing the mechanics of dealing with threads via the facilities supplied in .NET. All valuable advice and things you have to learn but it will really help if you become more familiar with the "theory" first. Take the cross-threading issue. What's really going on is the UI thread has some built-in sophistication that checks to see if you are modifying a control from a different thread. The designers at MS realized this was an easy mistake to make so the built in protection against it. Why is it dangerous? That requires you understand what an atomic operation is. Since the control's "state" can't be changed in an atomic operation, one thread could start to change the control and then another thread could become active, thus leaving the control in a partially modified state. Now if you tell the UI I don't give a crap just let my thread modify the control, it might work. However, when it doesn't work, you'll have a very hard to find bug. You'll have made your code much less maintainable and the programmers that follow you will curse your name.
So the UI is sophisticated and checks for you, the classes and threads you are writing don't. You have to understand what is atomic in your classes and threads. You might be causing the "freeze" in your threads. Most commonly an app freezing is the result of a deadlock condition. In this case "freeze" means the UI never becomes responsive again. At any rate this is getting too long but I think at a minimum you should be familiar with the use of "lock" and probably also Monitor, interlocked, semaphore and mutex.
Just to give you an idea: (from Troelsen's book)
intVal++; //This is not thread safe
int newVal = Interlocked.Increment(ref intVal); //This is thread safe
.NET also provides a [Synchronization] attribute. This can make writing a thread safe class easy, but you do pay a price in efficiency for using it. Well I'm just hoping to give you some idea of the complexities involved and motivate you to go do some further reading.
Do like this, create a new thread
Thread loginThread = new Thread(new ThreadStart(DoWork));
loginThread.Start();
Inside the ThreadStart(), pass the method you want to execute. If inside this method you want to change somes controls properties then create a delegate and point it to a method inside which you will be writing the controls changed properties,
public delegate void DoWorkDelegate(ChangeControlsProperties);
and invoke the controls properties do like this, declare a method and inside it define the controls new porperties
public void UpdateForm()
{
// change controls properties over here
}
then point a delegate to the method, in this way,
InvokeUIControlDelegate invokeDelegate = new InvokeUIControlDelegate(UpdateForm);
then when you want to change the properties at any place just call this,
this.Invoke(invokeDelegate);
Hope this code snippet helps you ! :)
To add onto freedompeace's solution, he was right. This solution is thread-safe
and that's exactly what you want. You just need to use BeginInvoke
instead of Invoke
.
void SetControlText(Control control, string text)
{
control.BeginInvoke(
new MethodInvoker(() =>
{
control.Text = text;
})
);
}
The above fix is the simplest and cleanest.
Hope this helps. :)
精彩评论