There are many occasions when programmers need to develop multithreading applications, which can load big files in the background and let the user do data entry or other jobs without any interruption. In this article, I'll show you how to create multiple threads to load multiple files.
In this example we are going to study how we can do such a thing. We will be reading XML files and displaying them in a TreeView control. But, you can apply the same concept and do whatever else you want to do with them like initializing data variables etc. We could have done the same thing with a database rather than an XML file but to start with and keep things simple we will take this approach.
Note that we have two XML files (along with the source code) to understand the example.
The user-interface is shown below:
The class filedisplayer helps in drawing the form shown above. The form consists of the buttons: select_button, process_button, abort_button and cancel_button. The button cancel_button helps in canceling the form, the button select_button, when clicked helps to open an OpenFileDialog control from where the user can select the file. The user can also manually enter the file name in the TextBox control.
private void selectbutton_click(object sender, System.EventArgs e)
{
// Handler for the TextFileOpen command
OpenFileDialog openFileDialog1 = new OpenFileDialog();
openFileDialog1.Filter = "All Files (*.*)|*.*|Text Files (*.txt)|*.txt";
if (openFileDialog1.ShowDialog () == DialogResult.OK)
{
String fileName = openFileDialog1.FileName;
// Since we want to open only xml files hence the condition.
if ( (fileName.Length != 0) && (fileName.EndsWith("xml")))
{
filename_box.Text=fileName;
}
}
}
Once a file is selected, the user has to click the process_button to start the file processing. As stated above we are just displaying the contents of the file in a TreeView control. The main objective (of this article) is to give the reader a fair idea of how to implement multi-threading while doing intensive work. The button abort_button helps to abort the task at any stage.
Now that we are familiar with what the different controls in the user-interface do, let us delve further into the intricacies.You would notice in the code that we have declared these lines (shown below):
private Thread QueueMonitorThread ;
private RequestQueue req_queue;
private bool m_bAbort;
private ThreadEventHandler onTreeViewElement;
We would soon understand what each one does.We have defined a struct called FileInfo as shown below (it is placed in the file RequestQueue.cs).
public struct FileInfo
{
public string fName;// you can define some other fields, if needed.
}
In this file, RequestQueue.cs we have also defined a class called RequestQueue that is a queue to hold the files names. This size of this queue is set to 5 in the example. So, the queue can have a maximum of five file names at any time. This queue has the logic to add new file names (add), remove the files that have already been sent for processing (remove), set its size (setSize), check its status (isEmpty) etc.
Note: You can also use the Queue class in System.Collections namespace rather than writing your own queue class.
Whenever the process_button is clicked, it extracts the filename from the TextBox control and adds it to the variable fName in the FileInfo struct. Finally, it adds that file name to the queue. So, the XML files that you need to process are thus queued for processing.
private void processbutton_click(object sender, System.EventArgs e)
{
FileInfo f = new FileInfo();
f.fName=this.filename_box.Text;
while ( req_queue.isEmpty()!=1)
//Wait for space ....If Queue is full then we can't add element to it
{
if( req_queue.isEmpty() == 1 )
break;
Thread.Sleep(200);
}
req_queue.add(f);
}
When the form is initialized, we create and start a thread called QueueMonitorThread that calls the function QueueMonitorfunc.
public filedisplayer()
{
InitializeComponent();
req_queue = new RequestQueue();
req_queue.setSize(5);
m_bAbort = false;
QueueMonitorThread = new Thread( new ThreadStart(QueueMonitorfunc));
QueueMonitorThread.Start();
onTreeViewElement = new ThreadEventHandler(populateTreeView);
}
The purpose of this thread is to check the queue, the moment there is a file in the queue it calls a function parse sending the filename as a parameter to it. It then calls remove method of the RequestQueue class to remove the file from the queue.
public void QueueMonitorfunc()
{
while( true)
{
if(isAbort()) //Check to see if someone has pressed abort button ...
{
break;
}
Object o = req_queue.getFile();
if( (o is FileInfo ))
{
FileInfo f = (FileInfo)req_queue.getFile();
string filename = f.fName;
parse(filename);
req_queue.remove();
}
Thread.Sleep(500);
}
}
Note that the thread, QueueMonitorThread, does not itself do any processing of the file. Its only purpose is to detect the presence of a file in the queue that once detected is sent to the parse method for further processing. The parse method actually creates more threads with each thread parsing one file.
The parse method is shown below:
private void parse(FileInfo info)
{
Thread t = parserThread.CreateThread (new parserThread.Start(parserMethod), info);
t.Start ();
t.Join (Timeout.Infinite);
}
It creates a thread by calling the static method CreateThread of the parserThread class. It then starts the thread and also invokes the Join method on the thread to prevent the thread from terminating itself mid-way. This is the thread that does the actual processing of the file.
The parserThread class is shown below:
public class parserThread
{
public delegate void Start (object obj);
private class Argument
{
public object obj1;
public Start s1;
public void parse()
{
s1(obj1);
}
}
public static Thread CreateThread (Start s, Object arg1)
{
Argument arg = new Argument();
arg.obj1 = arg1;
arg.s1 = s;
Thread t = new Thread (new ThreadStart (arg.parse));
return t;
}
}
We are creating a thread this way unlike the usual way (as done with the thread QueueMonitorThread) because we need to pass a parameter to the thread (which is the filename). Threads should be able to call methods that take parameters. Since the ThreadStart delegate takes only one parameter that is a state object hence we need to convey parameters via this object to the invoked method.
Let us dissect the code in the line shown below (in the parse method):
Thread t = parserThread.CreateThread(
new parserThread.Start(parserMethod), info);
We are instantiating a delegate called Start (in the parserThread class) passing a parserMethod and an instance of struct FileInfo. The code for parserMethod is shown below:
public void parserMethod(object obj)
{
FileInfo fInfo = (FileInfo)obj;
process_xml((fInfo.fName));
}
if you see the CreateThread method in the parserThread class, the parserMethod shown above would immediately become clear. Hence we have been able to successfully pass the parameter, which is the filename, to the thread. This method (parserMethod) calls the process_xml method passing it the file to be processed. This method is shown below:
public void process_xml(String name)
{
try
{
XmlDocument doc = new XmlDocument();
String fname = name;
doc.Load(fname);
XmlNodeList nList1;
XmlNodeList nList2;
XmlNodeList nList;
nList=doc.GetElementsByTagName("EmpDataSet");
for( int m =0;m<nList.Count;m++)
{
XmlElement element_main = (XmlElement)nList.Item(m);
nList1 = element_main.ChildNodes ;
for( int k =0;k<nList1.Count;k++)
{
XmlElement element_fchild = (XmlElement)nList1.Item(k);
nList2 = element_fchild.ChildNodes ;
IEmpDetails emp = new EmpDetails();
if( m_bAbort)
{
return;
}
for( int j =0;j<nList2.Count;j++)
{
XmlElement child_element = (XmlElement)nList2.Item(j);
if( child_element.Name == "Emp_id" )
{
emp.empId = System.Convert.ToInt32(child_element.InnerText);
}
if( child_element.Name == "Emp_Name" )
{
emp.empName = child_element.InnerText;
}
if( child_element.Name == "Emp_Address" )
{
emp.empAddress = child_element.InnerText
}
if ( child_element.Name == "Emp_City" )
{
emp.empCity = child_element.InnerText;
}
if( child_element.Name == "Emp_State" )
{
emp.empState = child_element.InnerText;
}
if( child_element.Name == "Emp_Pin" )
{
emp.empPin = child_element.InnerText;
}
if( child_element.Name == "Emp_Country" )
{
emp.empCountry = child_element.InnerText;
}
else
if( child_element.Name == "Emp_Email" )
{
emp.empEmail = child_element.InnerText;
}
}
BeginInvoke(onTreeViewElement, new object[] {this, new ThreadEventArgs(emp)});
}
}
}
catch(Exception exp)
{
MessageBox.Show("Error...in displaying treeview "+exp.Message);
}
}
This is the normal processing to show the XML document in the TreeView. Here we extract the values from the XML file and populate the class EmpDetails. This class inherits the interface IEmpDetails. A portion of the class and interface are shown below:
public interface IEmpDetails
{
string empName
{
get;
set;
}
int empId
{
get;
set;
}
// and more fields
}
public class EmpDetails : IEmpDetails
{
private string empname;
private int empid;
// and more fields
public EmpDetails()
{
}
public string empName
{
get
{
return empname;
}
set
{
empname = value;
}
}
public int empId
{
get
{
return empid;
}
set
{
empid = value;
}
}
// and more fields
}
The interesting part to be noted is the function BeginInvoke. As you must have observed that the method process_xml only populates the data structure: EmpDetails but not the TreeView control. This is because the TreeView control is owned by the main thread and hence cannot be directly accessed by any other thread. To fill the TreeView control we need to use either the methods Invoke or BeginInvoke. This is because the Windows forms uses the single-threaded apartment (STA) model and this kind of model requires that any methods on a control that need to be called from outside the control's creation thread must be marshaled to the control's creation thread. Marshaling a method requires the equivalent of a function pointer or callback. This is accomplished using delegates in the .NET Framework. BeginInvoke therefore, takes a delegate (called onTreeViewElement) as an argument.
The method BeginInvoke asynchronously executes the specified delegate (onTreeViewElement) with the specified arguments on the thread that the controls underlying handle was created on. The delegate onTreeViewElement invokes the method populateTreeView as shown below:
private void populateTreeView(object sender, ThreadEventArgs e)
{
IEmpDetails ex = e.empDetails;
TreeNode n = new TreeNode("EMP :"+ex.empId);
n.Nodes.Add(ex.empName);
n.Nodes.Add(ex.empAddress);
n.Nodes.Add(ex.empCity);
n.Nodes.Add(ex.empState);
n.Nodes.Add(ex.empPin);
n.Nodes.Add(ex.empCountry);
n.Nodes.Add(ex.empEmail);
treeView1.Nodes.Add(n);
}
As seen it is the BeginInvoke method that actually populates the TreeView control via a call to a delegate (onTreeViewElement ) which invokes a method (populateTreeView) that takes parameters of the same number and type as that contained in the second parameter of BeginInvoke. The second parameter of BeginInvoke contains an array of objects (the instance of the current window and an instance of the ThreadEventArgs class (inherited from EventArgsand shown below)) to pass as arguments to populateTreeView method. To populate the TreeView control we need the data structure EmpDetails (that had been populated in the process_xml method) and hence the utility of the second argument in the function BeginInvoke.
public class ThreadEventArgs : EventArgs
{
IEmpDetails _empDetails;
public IEmpDetails empDetails
{
get
{
return _empDetails;
}
}
public ThreadEventArgs(IEmpDetails empDetails)
{
this._empDetails = empDetails;
}
}
Conclusion:
This example will have real utility in displaying extremely big files. This article has one limitation in the sense that once the abort_button is clicked, the application is aborted and you cannot run any more threads. You then have to restart the application.
Hope the article was useful in explaining a few concepts (particularly how to invoke a thread with parameters and how to create multiple threads to handle tasks.).