Lengthy Operations on Single Thread in .NET Application
Wiktor Zychla [wzychla_at_ii.uni.wroc.pl]
The problem
Desktop applications are usually single-threaded. Sometimes it is, however, inevitable to perform some long lasting operations like processing large XMLs.
This leads to a common problem: how such long lasting operations should be managed?
If you try to search for an answer, you'll find that there are two possibilities:
- perform lengthy operation on a new thread
- perform lengthy operation on the same thread
The former possibility involves threading issues such as synchronizing threads and updating the GUI from other threads. However, I've found that this possibility
is preferred over the latter. There is a main reason for this: the
lengthy operation cannot be easily interrupted in a single-thread
application.
Below I will try to show you that this problem can be solved.
The solution
The solution I suggest is rather simple. You have to force the lengthy
operation to scan the main message loop of the application (Application.DoEvents).
This way you can inform the lengthy operation that it should stop using
some external information. In the example below, the lengthy operation
is controlled
by CanContinue variable. The variable value is changed upon the user interaction. Note that the ButtonStop_Click event is executed
from within the LengthyOperation indirectly - it is the
Application.DoEvents that dispatches all pending Win32 events, in our case
it forces the button click event to be executed.
public bool CanContinue = true;
public void LengthyOperation()
{
while ( true )
{
// check
if ( !CanContinue ) return;
// force all pending messages to be dispatched
Application.DoEvents();
// do the lengthy job
...
}
}
public void ButtonStart_Click( object sender, EventArgs e )
{
LengthyOperation();
}
public void ButtonStop_Click( object sender, EventArgs e )
{
CanContinue = false;
}
New problems appear
The above solution seems to be correct until we realize that this is not only a pending button-click event that is executed by Application.DoEvents.
In fact, all pending Win32 events are executed by Application.DoEvents call. This causes two new problems to appear:
- if Application.DoEvents causes another lengthy
operation to invoke then the interruption-problem reappears. In particular cases it could
even lead to infinite recursion:
public void ButtonStart_Click( object sender, EventArgs e )
{
LengthyOperation();
}
public void ButtonStop_Click( object sender, EventArgs e )
{
// instead of stopping the operation
// risk an infinite recursion
LengthyOperation();
}
- if the user tries to close the main application window (by
pressing the x in the upper-right corner of the window) during the
lengthy operation
an event will occur in the application's main loop. This event will also be handled by Application.DoEvents. This could be a disaster! Instead of
interrupting the lengthy operation, the user could shut the
application down.
New problems go away
Careness to the rescue
First one of above problems can be avoided. All you have to do is to not to allow the user to press "dangerous" buttons or menu items so that he is unable to
invoke new operations. For example:
public void ButtonStart_Click( object sender, EventArgs e )
{
// do not let the user to start another instance of the operation while one is in progress
ButtonStart.Enabled = false;
LengthyOperation();
// it is safe to start new operation now
ButtonStart.Enabled = true;
}
Reflection to the rescue
The second problem is more subtle. The lengthy operation could be invoked
any context so at first glace there's no simple way
to stop Application.DoEvents to process events of the main form (and prevent it from closing the main form).
Note, however that when Application.DoEvents is invoked from
within the lengthy operation then the LengthyOperation's
frame is
still on the stack! And we can examine the stack using the reflection!
This is then how we prevent the main form from beeing closed by careless user (and Application.DoEvents): when the main form is about to be closed
we check the stack trace and look for methods that are marked as uninterruptable. If at least one such method is found then we are sure that an operation that
should not be interrupted is in progress. We then cancel the closing.
public class UnInterruptable : Attribute {}
// -------------------------------------------------------------------------
public static bool IsInterruptionPossible()
{
StackTrace st = new StackTrace();
for ( int i=0; i<st.FrameCount; i++ )
{
StackFrame sf = st.GetFrame(i);
MethodBase mb = sf.GetMethod();
foreach ( Attribute a in mb.GetCustomAttributes(true) )
{
if ( a is UnInterruptable )
return false;
}
}
return true;
}
// -------------------------------------------------------------------------
public bool CanContinue = true;
// mark the operation as uninterruptable
[UnInterruptable()]
public void LengthyOperation()
{
while ( true )
{
// check
if ( !CanContinue ) return;
// force all pending messages to be dispatched
Application.DoEvents();
// do the lengthy job
...
}
}
public void ButtonStart_Click( object sender, EventArgs e )
{
LengthyOperation();
}
public void ButtonStop_Click( object sender, EventArgs e )
{
CanContinue = false;
}
// -------------------------------------------------------------------------
public MainForm_Closing( object sender, , System.ComponentModel.CancelEventArgs e)
{
// check if there are some uninterruptable operations in progress
if ( !IsInterruptionPossible() )
{
e.Cancel = true;
return;
}
}
Conclusions
In this article I've shown how the lengthy operations can be handled in
a
.NET application. I've also shown how the stack trace can be examined to find any
specific methods. I hope this article will be usable to the reader.