Friday, August 22, 2008

Threading with .NET ThreadPool Part 4

Suppose you've been given the task to write a function that copies the contents of one folder to another. So you set off on your merry way and come up with something like the following.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;

namespace ThreadPoolPart4
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
// Get the Folder names, copy contents from one to the other
FolderBrowserDialog fb = new FolderBrowserDialog();
fb.ShowDialog();
string src = fb.SelectedPath;
fb.ShowDialog();
string dst = fb.SelectedPath;
// no error checking on the names, this is an example only

if (dst != src)
{
if (!Directory.Exists(dst))
Directory.CreateDirectory(dst);
RecurseCopyFolder(new DirectoryInfo(src), new DirectoryInfo(dst), MyCopyCallback);
}
}
private void RecurseCopyFolder(DirectoryInfo src, DirectoryInfo dst, CopyCallbackDelegate cb)
{
bool cancelled = false;
CopyArgs ca;
try
{
foreach (FileInfo fi in src.GetFiles())
{
string newfile = Path.Combine(dst.FullName, fi.Name);

ca = new CopyArgs() { IsDir = false, CurrentObject = newfile };
cb(ref cancelled, ca);
if (cancelled)
break;
if (!ca.Skip)
{
if (Directory.Exists(ca.CurrentObject))
Directory.Delete(ca.CurrentObject);
fi.CopyTo(ca.CurrentObject, true);
}

}
if (!cancelled)
foreach (DirectoryInfo subsrc in src.GetDirectories())
{
ca = new CopyArgs() { IsDir = true, CurrentObject = Path.Combine(dst.FullName, subsrc.Name) };
cb(ref cancelled, ca);

if (cancelled)
break;
if (ca.Skip)
continue;
DirectoryInfo subdst;
if (!Directory.Exists(ca.CurrentObject))
subdst = dst.CreateSubdirectory(subsrc.Name);
else
subdst = new DirectoryInfo(ca.CurrentObject);

RecurseCopyFolder(subsrc, subdst, cb);
}
}
catch (Exception e)
{
throw e;
}
}

public class CopyArgs : EventArgs
{
public bool IsDir;
public bool Skip;
public string CurrentObject;
}

private delegate void CopyCallbackDelegate(ref bool Cancel, CopyArgs args);
private void MyCopyCallback(ref bool Cancel, CopyArgs args)
{
if (args.IsDir)
label1.Text = args.CurrentObject;
else
label2.Text = args.CurrentObject;
}

}
}


But when you test this on a somewhat larger folder, your application appears to hang. What gives? Well, it's very common for programmers to hang up their GUI by waiting, sleeping, reading the network, or in this case, doing intensive file I/O operations on the GUI thread. It's been said hundreds if not thousands of times on the forums, "don't Sleep() in the GUI thread". Because, if your GUI gets stuck in a Sleep() or anywhere else, it does not get a chance to pump its message loop. A GUI that does not service its message queue looks like it is dead. That's why sending your GUI down a recursive file copy excursion like we just did is a really bad idea.

That is also why threading has become a best practice for C# application development. And today, we take another look at the System.Threading.ThreadPool. In my previous articles on the ThreadPool ("Threading with .NET ThreadPool, Part 1, Part 2 and Part 3"), I spent the entire time talking about ThreadPool.QueueUserWorkItem(). But, there are other ways within your power to put the ThreadPool to work for you. Specifically, I am talking about the delegate.BeginInvoke() call. Let us see how it is used.

First off, to solve our dead looking GUI application (if your's doesn't look dead, you didn't try copying a big enough folder), we need to put the "heavy lifting" into a thread. All the hard work occurs in RecurseCopyFolder(), so we will put that call into a thread using BeginInvoke(). We do it by declaring a delegate for the function, then calling the delegate's BeginInvoke() like so...


private delegate void RecurseCopyFolderDelegate(DirectoryInfo src, DirectoryInfo dst, CopyCallbackDelegate cb);
private void RecurseCopyFolder(DirectoryInfo src, DirectoryInfo dst, CopyCallbackDelegate cb)
{ //...
}


You should be familiar with delegates, but if not, just note how the delegate call signature is exactly the same as the function it will delegate for. I name them similarly for the sake of self-documenting code.

Next, we create an instance of the delegate. I create a class scope variable to hold the delegate instance because I need it later. I also create an IAsyncResult variable because I will be needing that, too when the thread completes.


RecurseCopyTreeDelegate dlgt;
IAsyncResult asyncResult;


Now, we instantiate the delegate and get a new thread started with BeginInvoke(). Notice that BeginInvoke() takes the same parameters as the original function (plus two, we'll come back to those later). The "delegate" is implemented by the compiler, so the compiler also does us the favor of letting us call the BeginInvoke() almost like we would call the delegate itself.


// RecurseCopyFolder(new DirectoryInfo(src), new DirectoryInfo(dst), MyCopyCallback);
dlgt = new RecurseCopyFolderDelegate(RecurseCopyFolder);
asyncResult = dlgt.BeginInvoke(new DirectoryInfo(src), new DirectoryInfo(dst), MyCopyCallback, null, null);


We are almost ready to run, but notice we are using a callback function to update our GUI. You never update the GUI directly from a different thread, its not thread-safe. So we will add the code to make the callback check the form's InvokeRequired property and Invoke() the call if necessary. It is common to have one function check the InvokeRequired and call itself again with Invoke(), so I've modified MyCopyCallback() to do just that.


private delegate void CopyCallbackDelegate(ref bool Cancel, CopyArgs args);
private void MyCopyCallback(ref bool Cancel, CopyArgs args)
{
if (this.InvokeRequired)
{
this.Invoke(new CopyCallbackDelegate(MyCopyCallback), new object[] { Cancel, args });
}
else
{
if (args.IsDir)
label1.Text = args.CurrentObject;
else
label2.Text = args.CurrentObject;
}
}


There's still one more requirement for using BeginInvoke(). When using the "delegate" BeginInvoke(), you are required to call the delegate's EndInvoke() when the thread completes. So, in order to know when the function completes, you can use a timer (remember to enable it) and check the asyncResult variable...


private void timer1_Tick(object sender, EventArgs e)
{
if(asyncResult != null)
if (asyncResult.IsCompleted)
{
dlgt.EndInvoke(asyncResult);
asyncResult = null;
timer1.Enabled = false;
}
}


But, even better than a timer would be to use those two nice little parameters at the end of BeginInvoke(). They are the "Thread Completion Callback" and the "User Data Object". You supply a callback that will get called when the thread completes. It is called with the user data object passed in inside of the IAsyncResult parameter. We normally pass the delegate itself as the user data object so that we can use it to call the EndInvoke() with. This allows us to eliminate those class scope variables we added earlier (though I don't, I'll leave it as an exercise). So we still have to write a completion routine.


public void RecurseCopyDone(IAsyncResult result)
{
if (this.InvokeRequired)
{
this.Invoke(new AsyncCallback(RecurseCopyDone), new object[] { result });
}
else
{
RecurseCopyFolderDelegate d = result.AsyncState as RecurseCopyFolderDelegate;
d.EndInvoke(result);
label1.Text = "Done";
label2.Text = "Done";
}
}


The Invoke() above is not necessary to call the delegate's EndInvoke(), but it is needed to update my labels, so I opt to keep all the code together. EndInvoke() can be run from either the GUI thread or the callback thread.

So, there you have it. The complete source that follows of the Form1.cs and the Form1.Designer.cs has an additional button allowing us to cancel the operation. Copying files the way I am doing it cannot be interrupted, so cancellation occurs between files. The final version uses the IAsyncCallback instead of a timer. I hope this has been of use to you. Here's the code...

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;

namespace ThreadPoolPart4
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private bool CancelCopyFlag = false;
private IAsyncResult asyncResult = null;
private RecurseCopyFolderDelegate dlgt;
private void button1_Click(object sender, EventArgs e)
{
CancelCopyFlag = false;

// Get the Folder names, copy contents from one to the other
FolderBrowserDialog fb = new FolderBrowserDialog();
fb.ShowDialog();
string src = fb.SelectedPath;
fb.ShowDialog();
string dst = fb.SelectedPath;
// no error checking on the names, this is an example only

if (dst != src)
{
if (!Directory.Exists(dst))
Directory.CreateDirectory(dst);
// RecurseCopyFolder(new DirectoryInfo(src), new DirectoryInfo(dst), MyCopyCallback);
dlgt = new RecurseCopyFolderDelegate(RecurseCopyFolder);
asyncResult = dlgt.BeginInvoke(new DirectoryInfo(src), new DirectoryInfo(dst), MyCopyCallback, new AsyncCallback(RecurseCopyDone), dlgt);
}
}
private delegate void RecurseCopyFolderDelegate(DirectoryInfo src, DirectoryInfo dst, CopyCallbackDelegate cb);
private void RecurseCopyFolder(DirectoryInfo src, DirectoryInfo dst, CopyCallbackDelegate cb)
{
bool cancelled = false;
CopyArgs ca;
try
{
foreach (FileInfo fi in src.GetFiles())
{
string newfile = Path.Combine(dst.FullName, fi.Name);

ca = new CopyArgs() { IsDir = false, CurrentObject = newfile };
cb(ref cancelled, ca);
if (cancelled)
break;
if (!ca.Skip)
{
if (Directory.Exists(ca.CurrentObject))
Directory.Delete(ca.CurrentObject);
fi.CopyTo(ca.CurrentObject, true);
}

}
if (!cancelled)
foreach (DirectoryInfo subsrc in src.GetDirectories())
{
ca = new CopyArgs() { IsDir = true, CurrentObject = Path.Combine(dst.FullName, subsrc.Name) };
cb(ref cancelled, ca);

if (cancelled)
break;
if (ca.Skip)
continue;
DirectoryInfo subdst;
if (!Directory.Exists(ca.CurrentObject))
subdst = dst.CreateSubdirectory(subsrc.Name);
else
subdst = new DirectoryInfo(ca.CurrentObject);

RecurseCopyFolder(subsrc, subdst, cb);
}
}
catch (Exception e)
{
throw e;
}
}

public void RecurseCopyDone(IAsyncResult result)
{
if (this.InvokeRequired)
{
this.Invoke(new AsyncCallback(RecurseCopyDone), new object[] { result });
}
else
{
RecurseCopyFolderDelegate d = result.AsyncState as RecurseCopyFolderDelegate;
d.EndInvoke(result);
label1.Text = "Done";
label2.Text = "Done";
}
}

public class CopyArgs : EventArgs
{
public bool IsDir;
public bool Skip;
public string CurrentObject;
}

private delegate void CopyCallbackDelegate(ref bool Cancel, CopyArgs args);
private void MyCopyCallback(ref bool Cancel, CopyArgs args)
{
if (this.InvokeRequired)
{
this.Invoke(new CopyCallbackDelegate(MyCopyCallback), new object[] { Cancel, args });
}
else
{
if (args.IsDir)
label1.Text = args.CurrentObject;
else
label2.Text = args.CurrentObject;
Cancel = CancelCopyFlag;
if (Cancel)
{
label1.Text = "Cancelled";
label2.Text = "Cancelled";
}
}
}

private void button2_Click(object sender, EventArgs e)
{
CancelCopyFlag = true;
}
}
}


Form1.Designer.cs

namespace ThreadPoolPart4
{
partial class Form1
{
private System.ComponentModel.IContainer components = null;

protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.button1 = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.button2 = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(18, 17);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 23);
this.button1.TabIndex = 0;
this.button1.Text = "button1";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(22, 71);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(35, 13);
this.label1.TabIndex = 1;
this.label1.Text = "label1";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(22, 113);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(35, 13);
this.label2.TabIndex = 2;
this.label2.Text = "label2";
//
// button2
//
this.button2.Location = new System.Drawing.Point(169, 22);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(75, 23);
this.button2.TabIndex = 3;
this.button2.Text = "button2";
this.button2.UseVisualStyleBackColor = true;
this.button2.Click += new System.EventHandler(this.button2_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(292, 266);
this.Controls.Add(this.button2);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
this.PerformLayout();

}

#endregion

private System.Windows.Forms.Button button1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Button button2;
}
}

No comments: