Wednesday, October 01, 2008

Does Multi-Threaded Winforms application really exist?

For my thesis, I wanted a powerful MDI application. By powerful, I mean which can process at least 100 child forms at a time. Each form does a bunch of background work and for this reason I wanted a truly multithreaded windows application. So I went ahead and spawned the child forms using ThreadPool.

The child form is pretty simple for this example, it does not do any major background work. It just has a browser control and when clicked on a menu item, it sleeps for 10 seconds and sets the text of the form to "Done". I have two menu options, one to do work in the background and one which does not do that work in the background. But when you launch a Multithreaded MDI, you expect the work to be done in the background, irrespective of the way you code it.

private void someWorkNotInBackgroundToolStripMenuItem_Click(object sender, EventArgs e)
{
Thread.Sleep(10000);
this.Text = "Done";
}
A true multithreaded application should not stuck up other child windows when it does the above shown work.


But that is not the case. So you have do something like shown below, for the application to be truly responsive irrespective of the background work it does.



private void someWorkInBackgroundToolStripMenuItem_Click(object sender, EventArgs e)
{
new MethodInvoker(() => Thread.Sleep(10000)).BeginInvoke(new AsyncCallback(it => this.Text = "Done"), null);
}

A delegate is used to asynchronously perform background jobs inside a windows form. Doing so does not stuck up the application unlike the first case.


Using ThreadPool to spawn threads



Well, if you want a truly multithreaded application, then you have do all tasks in background using BeginInvoke() as shown the previous code snippet. Spawning child threads to create MDI Children would not help. But anyway, in this section, I show you how to use ThreadPool and just because we are talking about Multithreaded Windows, I show the windows in the worker thread.



Firstly, in order to use thread pool, we need a worker item. For this worker item, it is recommended you create a class that represents this worker item. My worker item is shown below.



class ThreadPoolWorkerItem
{
/// <summary>
///
The tracker supposedly keeps track of the worker item status.
///
Though in this example, it does not work. Usually in the work method,
///
you do a Set() on this tracker which signals the WaitHandle.
/// </summary>
private ManualResetEvent tracker;

/// <summary>
///
Initializes a new instance of the <see cref="ThreadPoolWorkerItem"/>
class.
/// </summary>
/// <param name="tracker">
The tracker.</param>
14:
public ThreadPoolWorkerItem(ManualResetEvent tracker)
{
this.tracker = tracker;
}

/// <summary>
///
Performs the work done by the current ThreadPool Worker Item.
/// </summary>
/// <param name="threadState">
State of the thread.
</param>
public void work(object threadState)
{
ChildForm cf = threadState as ChildForm;
//cf.Show() is invalid. It throws an InvalidOperationException, since cf was not created in this thread.
cf.Invoke(new MethodInvoker(() => cf.Show()));
cf.FormClosed += new FormClosedEventHandler(cf_FormClosed);
//The two statements below simulate a job that takes 1 second to work and then it notifies using the tracker.
//That is the way usually ThreadPool worker items should work.
//Thread.Sleep(1000);
//tracker.Set();
}

/// <summary>
///
Handles the FormClosed event of the cf control.
///
In this event, we supposedly inform the waiting thread about the current worker item's job done.
///
It does not work in this example.
/// </summary>
/// <param name="sender">
The source of the event.
</param>
/// <param name="e">
The <see cref="System.Windows.Forms.FormClosedEventArgs"/> instance containing the event data.
</param>
void cf_FormClosed(object sender, FormClosedEventArgs e)
{
tracker.Set();
}
}

The code explains how threadpool worker items should be created.


The code for adding worker items to the ThreadPool is shown below.





   1: /// <summary>


   2: /// Does the work.It creates ThreadPoolWorkerItems and adds it to the ThreadPool.


   3: /// </summary>


   4: private void DoWork()


   5: {


   6:     ThreadPool.SetMaxThreads(2, 2);


   7:     ManualResetEvent[] trackers = new ManualResetEvent[Maxthreads];


   8:     for (int i = 0; i < Maxthreads; i++)


   9:     {


  10:         trackers[i] = new ManualResetEvent(true);


  11:         ThreadPoolWorkerItem item = new ThreadPoolWorkerItem(trackers[i]);


  12:         this.Invoke(new MethodInvoker(() =>{                                                  {


  13:                      ThreadPool.QueueUserWorkItem(item.work, new ChildForm("http://google.com") { MdiParent = this });


  14:                 }));


  15:     }


  16:     WaitAll(trackers);


  17: }




The code explains how you actually create and add worker items to  thread pool. In this case using a "ThreadPool.QueueUserWorkItem(item.work,new ChildForm("http://google.com"){MdiParent = this});" would give a ThreadStateException (for other cases, you might see a InvalidOperationException). For that reason, a this.Invoke() is used to create the child form and spawn it.



So you schedule some jobs in the ThreadPool and you should wait for them to complete. So how do you wait? We use WaitHandle.WaitAll(trackers); which suspends the current thread until all the spawned worker items in the ThreadPool are completed, whose manual reset events are the "trackers".



But in our case, it does not work. So I thought, it has some issues and researched online. I found an alternative WaitAll() to be used in Windows Forms.





   1: /// <summary>


   2: /// Waits on all the wait handles to complete execution.


   3: /// The WaitHandle in this case is a manual reset event.


   4: /// </summary>


   5: /// <param name="waitHandles">The wait handles.</param>


   6: private void WaitAll(ManualResetEvent[] waitHandles)


   7: {


   8:     foreach (WaitHandle myWaitHandle in waitHandles)


   9:         WaitHandle.WaitAny(new WaitHandle[] { myWaitHandle });


  10: }




The WaitAll() to be used in Windows Forms. Note that none of the concepts explained would really create a multi-threaded windows forms applications where each child form run in its own thread. But the information I obtained is shared here.



Using Threads without ThreadPool



Threads could directly be created and spawned which shows the Child Forms. Something like that could be done as shown.





   1: for (int i = 0; i < Maxthreads; i++)


   2: {


   3:         new Thread(() =>{


   4:            this.Invoke(new MethodInvoker(() => 


   5:                 new ChildForm("http://google.com") { MdiParent = this }.Show()));


   6:                          }).Start();


   7: }




We create a thread which again delegates its job of creating a child window the the parent form. So is this going to be helpful? So can you create a truly multithreaded MDI application?



Again, I repeat, from what I know and from what I researched, you cannot create Child forms in an MDI application where each Child runs in its own thread. The form UI thread has to be spawned from the parent MDI form itself, thereby making it impossible for the thread to survive. As soon as the Show() is done, the thread dies, because it has nothing else to do!



Using Application.Run() to create child MDI forms



First of all, this is something I found out just now. So it has to be lame and you should probably not use this at any cost. But anyway here the code is.





   1: /// <summary>


   2: /// Handles the Click event of the launchUsingApplicationRunToolStripMenuItem control.


   3: /// It launches the Child windows using Application.Run().


   4: /// For it work, you should set the AparmentState of the thread to STA. Right now, I use a depricated way


   5: /// to do that. Optionally you can add a method with attribute set to STAThread, like in the Program.cs Main()


   6: /// </summary>


   7: /// <param name="sender">The source of the event.</param>


   8: /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>


   9: private void launchUsingApplicationRunToolStripMenuItem_Click(object sender, EventArgs e)


  10: {


  11:     for (int i = 0; i < 10; i++)


  12:     {


  13:         new Thread(() =>


  14:                        {


  15:                            ChildForm cf = new ChildForm("http://google.com") { };


  16:                            this.Invoke(new MethodInvoker(() => cf.MdiParent = this));


  17:                            Application.Run(cf);


  18:                        }) { ApartmentState = ApartmentState.STA }.Start();


  19:     }


  20: }




Do no use this. It just shows how retard I am.



Finally, how to make Multithreaded MDI?



From what I know, you cannot make a child form run in its own thread. But you can make the work it performs using asynchronous calls or threads.



So for every heavy job your child form does, you have spawn a thread or use asynchronous calls, iff you want your application to be responsive.



Disclaimer: I am no guru either in WinForms or Multi-threading. But I tried to share what I learnt writing applications for my thesis and job.

2 comments:

Josh said...

The SetParent method in the Win32 api is your friend. It lives in User32.dll - visit pinvoke.net to see how to use it in c#.

The standard MDI infrastructure provided by winforms isn't able to be used with Forms created on different threads, but you can create MDI applications where each 'I' runs on its on thread, you just have to handle pretty much everything yourself, and with kid-gloves.

Anonymous said...

Неприятная история у меня произошла... ((
Два года жила с парнем. Стала замечать странности в его поведении... Но серьёзных причин подозревать его в чём-то не было, пока однажды он нечаянно не назвал меня Аней (меня зовут Марина)... А позавчера случайно услышала, как он по телефону разговаривал с какой-то Аней ((((( На мои вопросы кто это - сказал, что одногруппница позвонила.
Посоветовалась с подружкой, как мне быть. Она дала мне адрес сайта http://telefon.center-vip.com с помощью которого она подловила своего прошлого парня, контролируя его sms-переписку и звонки.
Если бы Вы знали, что мне пришлось пережить, когда я почитала о чем мой переписывается с этой Аней!!!...Я БЫЛА В ШОКЕ!
Когда я показала ему распечатку, он даже не извинился ((( Просто сказал, что нам больше не нужно быть вместе....

Форумчане! Проверяйте своих любимых половинок, запомните сайт http://telefon.center-vip.com, он многим помог узнать правду... иногда горькую правду...

P.S. Если я неправильно выбрала раздел для темы..извините, пожалуйста. Мне просто хотелось с кем-то поделиться горем..