Craig said:
I am brand new to threads. I have created a small home at that uploads
a file to an FTP server. It froze up my GUI. So, I changed it to use a
thread to do the uploading:
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ParameterizedThreadStart
(Uploader.UploadFile));
thread.Start(files);
while (thread.IsAlive)
{
Thread.Sleep(1);
this.Invalidate();
this.Refresh();
}
this.Text = "Thread Done!";
}
The above code is little better than just executing your network i/o in
smaller chunks and calling Refresh() between chunks (which is to say,
not good at all). Also, calling Invalidate() before Refresh() is
redundant (Refresh() implies an invalidation of the entire control), and
the above doesn't actually "unfreeze" your GUI. It only forces it to
redraw every time the thread becomes runnable (which not only is a waste
of CPU time, but doesn't provide for any user input or interaction).
What you really need to do is start a thread in your Click event
handler, and then return immediately. Let the main GUI thread operate
normally (disable controls and/or operations as appropriate while the
worker thread is doing its processing). Then have some mechanism, if
needed, for the worker thread to report status back.
For example, call a method or otherwise execute some code that
encapsulates the logic that should execute when the thread is done (in
this case, using the Control.Invoke() method to ensure that the access
of the Text property is done on the correct thread):
class Form1
{
private void button1_Click(object sender, EventArgs e)
{
Button1Enabled = false;
new Thread(delegate()
{
Uploader.UploadFile(files, this);
}).Start();
}
// Public property so Uploader class can re-enable
// button. In reality, this is a pretty poor way to
// implement this behavior, due to the tying of the
// Uploader class to the Form1 class. But it's outside
// the scope of this example to demonstrate more-correct
// techniques for that particular issue.
public bool Button1Enabled
{
get { return button1.Enabled; }
set { button1.Enabled = value; }
}
}
class Uploader
{
// For the purpose of the example, I've simply passed the
// reference to the Form1 instance to this method.
// See comment for Form1.Button1Enabled property for
// why such a poor technique is shown here.
public static void UploadFile(List<string> files, Form1 form)
{
// do stuff
form.Invoke((MethodInvoker)delegate
{
form.Text = "Thread Done!";
form.Button1Enabled = true;
}
}
}
Or alternatively, if you use BackgroundWorker as Alberto suggests, just
add an event handler to the RunWorkerCompleted event for your
BackgroundWorker instance:
class Form1
{
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += (sender, e) =>
{
Uploader.UploadFile(files);
};
bw.RunWorkerCompleted += (sender, e) =>
{
this.Text = "Thread Done!";
button1.Enabled = true;
};
bw.RunWorkerAsync();
}
}
Note the use of lambda expressions to create anonymous methods, for
additional conciseness.
'files' is a List<> of files that will be uploaded.
This works well, but... I'd like to show a progress bar. Is there a
way for the thread to send a string back to the parent thread, which I
can the use to populate a Label?
The normal idiom in the System.Windows.Forms namespace is to use the
BackgroundWorker. Using that class, all interaction between the worker
thread and your main GUI thread is mediated via the BackgroundWorker.
I.e. with the ProgressChanged and RunWorkerCompleted events, both of
which are automatically raised on the main GUI thread.
Be careful to note, however, that while calling ReportProgress() raises
the ProgressChanged event on the main GUI thread (assuming the
BackgroundWorker instance is created on that thread), it uses a "post"
rather than a "send" to marshal the invocation to raise the event. What
that means is that your event handler is executed asynchronously
relative to the worker thread.
So while doing so automatically deals with the thread affinity issue
inherent in the System.Windows.Forms namespace (i.e. the issue that,
with the exception of a handful of members documented otherwise, all
access of members of Control and its subclasses must occur on the main
GUI thread), it does _not_ deal with synchronization for access to the
data shared by threads, because the ProgressChanged event handler can be
executing at the same time as your worker thread code (the DoWork event
handler, if you're using BackgroundWorker).
To address data synchronization, you can:
-- only pass immutable data or value types, such as a string or an
int, or...
-- deal with the cross-thread invocation more directly, by using
the Control.Invoke() method (so that the worker thread blocks until the
invoked delegate is done doing its work on the main GUI thread), or...
-- synchronize the data sharing somehow
My preference is generally the first option. It requires no specific
synchronization other than that required for the call to
ReportProgress(), and immutable or copied data is a tried-and-true
approach to safe inter-thread communication.
The second option has the benefit of being an easily-recognized pattern
in Forms code, but of course it can really slow processing down (even
more than normal synchronization), especially on a multi-CPU system,
because it winds up serializing not just access to shared data
structures, but across the entire operation being invoked.
The third option is stated simply, but of course there's a wide range of
way to actually implement the synchronization, so it's a misleadingly
simple statement.
As Alberto says, the "lock" statement is the
simplest, most straightforward approach, but even that requires some
care to use properly, and other techniques just get more complicated
form there.
Hope that helps.
Pete