Introduction
In this article we will analyze a problem which often strikes software solutions. That is how to improve responsiveness of objects that are generally not
guaranteed to reply to every request immediately.
For example, in network communications proxy class may be in a blocking call and
hence completely unaware of the outer world until current operation completes.
If it happened that some network operation took longer to complete, e.g. due to
temporary network delays, then network client might block the calling thread for
so long that user observes interface freezing. Even worse, if connection is
dropped without notice, client might have to wait for half a minute or so until
network timeout is reported back.
It would be utterly inappropriate to keep UI thread blocked or in any other way
disabled due to waiting for a network operation to complete. On the contrary, it
is of great importance to ensure that application will not freeze and that user
is able to initiate other network operations in parallel, even while previous
operation is currently blocked and waiting for the error to finally pop up or a
lengthy transfer to complete successfully.
Responsive application should be able to show progress of all active operations
and to allow user to cancel any operation gracefully without freezing. In the
text that follows we will demonstrate how responsiveness of the application can
be guaranteed when working with an object which is not fully responsive by its
own design. All operations initiated from main application thread (which might
include requests made by the user through GUI) will be executed with no delay.
User would never notice any problems regarding application responsiveness. Under
the hood, objects that are performing lengthy operations might become
unresponsive from time to time and specific wrappers will be used to control the
situation so that user does not notice these delays.
Example Model
In this section we will present a class which simulates a process executing in
time slices. It means that object of the class might execute a method in a
blocking mode, so that method does not exit immediately but only after a certain
amount of time. This is quite typical situation when contacting Web service or
sending data over the socket. However, code which is using this class would like
to be able to retrieve current status even when the class is executing the
blocking method. This is not a simple request because another call to the class
would have to be made from a different thread, which implicitly means that the
class must be thread safe. However, it is not the case in general. Here is the
listing of the class which simulates the process with a blocking method.
class
TimeSlicedWorker
{
public void
BeginWork(int totalSize)
{
_totalSize = totalSize;
_progress = 0;
_speedFactor = _rnd.Next(10) + 1;
}
public void
Advance(int amount)
{
_progress = (_progress + amount > _totalSize ? _totalSize :
_progress + amount);
System.Threading.Thread.Sleep(amount
* _speedFactor);
}
public int
GetWorkStatus() { return _progress; }
public int
GetProgressPercentage() { return (_totalSize > 0
? 100 * _progress / _totalSize : 0); }
public void
CancelWork() { _progress = _totalSize = 0; }
public bool
IsComplete() { return _progress >= _totalSize;
}
private int
_progress;
private int
_totalSize;
private int
_speedFactor;
private static
Random _rnd = new
Random();
}
This class simulates a lengthy work which can be modeled as increasing an
integer value (_progress) from zero to a predefined upper limit (_totalAmount).
Work begins with a non-blocking call to BeginWork method which specifies the
upper limit of the task. Further on, Advance method would be invoked potentially
many times, each time increasing current progress towards the _totalAmount
value, but also every time blocking for an uncertain period of time. Hence
Advance method simulates the blocking method which may freeze the calling thread
for some period of time.
In addition to these methods, caller may inspect the return value of IsComplete
method, which indicates whether _progress has reached _totalAmount. If so, the
work is done and caller may dispose the object. Otherwise, caller is free to
call CancelWork, which would reset the object and after this call no more calls
should be made to the object. Remaining two methods, GetWorkStatus and
GetProgressPercentage, are used to read current progress and show it to the
user.
Now what happens in practical case is that calling thread becomes blocked in the
Advance method for, say, couple of seconds, and during that time other thread
should not call CancelWork or GetProgressPercentage methods, because this class
is not thread safe. As the result of this unfortunate situation, user interface
is not able to provide current progress to the user, nor is it able to allow
user to cancel operation at any time. Even if user requests cancellation of
current operation, that request will not be executed until current blocking call
exits, and that might not be any time soon. And that is the behavior of the
application which users regard as unresponsive interface.
In the next section we will provide another class which wraps TimeSlicedWorker
and provides everything it lacks: thread safety and responsiveness.
Wrapper Class
To resolve the responsiveness issue described above, we will provide another
class named Smoother, which will wrap an instance of TimeSlicedWorker class. In
addition to simple wrapping of the object, Smoother will also provide a
thread-safe interface, exposing non-blocking methods to the user interface.
Internally, calls to the TimeSlicedWorker would be made from a different thread.
That thread would be blocked most of the time, waiting for the TimeSlicedWorker
to complete blocking operations. But every time when it has control over
contained object, it will collect latest status information from it. In that
way, Smoother will be capable to provide current, or at least last known status,
of the worker at all times, even when its own thread which communicates to the
worker is blocked.
In addition to these features, Smoother is also able to receive the cancel
request. This request will be routed to the worker as soon as current blocking
operation ends. This means that worker will be informed to cancel further work
as soon as it becomes responsive. In the meantime, Smoother's CancelWork method
would immediately exit, allowing the user interface to continue operations
without freezing.
Here is the full listing of the Smoother class.
class
Smoother
{
public Smoother()
{
_worker = new TimeSlicedWorker();
_worker.BeginWork(_rnd.Next(10000));
_sync = new
object();
_isComplete = new System.Threading.ManualResetEvent(false);
_thread = new System.Threading.Thread(new
System.Threading.ParameterizedThreadStart(ThreadProc));
}
public System.Threading.ManualResetEvent
Start()
{
_thread.Start(this);
return _isComplete;
}
private void
ThreadProc(object state)
{
Smoother smooth = (Smoother)state;
bool complete =
false;
lock (_sync)
{
_tsLastActivity = DateTime.UtcNow;
_percentProgress = _worker.GetProgressPercentage();
}
while (!complete)
{
if (IsWorkCancelled())
{
_worker.CancelWork();
complete = true;
}
else if
(_worker.IsComplete())
complete = true;
else
_worker.Advance(_rnd.Next(400));
// At this line _worker becomes
unresponsive for indefinite period of time
lock
(_sync)
{
_percentProgress = _worker.GetProgressPercentage();
_tsLastActivity = DateTime.UtcNow;
}
}
_isComplete.Set();
// Signal the outer world that working is over
}
public int
GetPercentProgress() { lock (_sync)
return _percentProgress; }
public void
CancelWork() { lock (_sync) _workCancelled =
true; }
private bool
IsWorkCancelled() { lock (_sync)
return _workCancelled; }
public
DateTime GetLastActivityTs() { lock
(_sync) return _tsLastActivity; }
public System.Threading.ManualResetEvent
GetWaitHandle() { return _isComplete; }
private object
_sync;
private System.Threading.Thread
_thread;
private TimeSlicedWorker _worker;
private bool
_workCancelled;
private System.Threading.ManualResetEvent
_isComplete;
private int
_percentProgress;
private
DateTime _tsLastActivity;
private static
Random _rnd = new
Random();
}
In the constructor, worker instance is allocated and initialized and thread is
created (but not started). Also, objects used in threads synchronization are
initialized. If TimeSlicedWorker needs to receive any initialization parameters,
this might be the place to pass them from the caller - either as Smoother
constructor arguments, or via additional Smoother's methods and properties.
When everything is in place, caller can invoke the Start method, which starts
the working thread and returns manually reset event on which caller can wait for
the worker to complete all work. This event, when set, signals that work was
completed, either by performing it to the end, or by successfully cancelling the
remaining work.
ThreadProc is the main method in the Smoother class. It calls worker's methods
until either all work is done or cancel is signaled. When either of the
conditions is met, thread procedure would simply set the event indicating work
completion and exit.
The Smoother class also provides public method GetPercentProgress, which is a
non-blocking method returning last known percentage progress. Note that result
of this method might not be up to date, but at any time it is the last available
value. Next method is CancelWork and it can be used to cancel further operations
on the contained worker. Note that this method only sets the internal flag
indicating that worker should cancel. It does not wait for the worker to
effectively exit. This is what makes the Smoother class responsive at all times.
However, caller must be aware that calling the CancelWork method does not
guarantee that work will be effectively cancelled at that specific moment.
Working might continue for quite a while after this method returns. So caller
must always wait at the event returned by the Start method (or by GetWaitHandle
method) in order to ensure that worker has been disposed. Last public method is
GetLastActivityTs, and it provides UTC timestamp of last communication of the
Smoother with its contained worker. If this timestamp differs too much from
current time, then worker can be declared unresponsive.
User Interface Example
We will not provide complete user application listing in this text because of
its length. However, this code is included in the attached source code, so
please feel free to download it and try it.
The example client allows user to start multiple workers in parallel. Each
worker will be wrapped in an instance of the Smoother class, ensuring that
interface will be responsive at all time. User will be able to contact each of
the objects, to read progress and to cancel work at will.
Here is the sample output of the program, which shows all available information.
Press 0-9 for unused slot to initiate worker
Press 0-9 for used slot to cancel work
Press Q to quit application
[0]
[1]
[2] Running (5%)
[3] Running (16%) [Not responding]
[4]
[5]
[6]
[7]
[8] Running (6%) [Not responding]
[9]
Pending close workers: 4
This output shows three workers started and progressing, two of them declared as
non responsive (i.e. they did not respond in more than one second). The last
line indicates that there are total of four workers waiting to cancel their
work. Their wrappers have already received the CancelWork request, but these
requests did not take effect yet because blocking operations have not been
completed on the workers.
Once again, please feel free to download and try the attached source code.
Conclusion
In this article we have demonstrated how a single-threaded blocking class can be
wrapped into a class with thread-safe non-blocking interface as a step towards
building a fully responsive interface. The source code provided is for
demonstration purposes only. It cannot be used as is in any particular
application, but ideas employed to design it are quite general and thus
applicable in wide range of practical applications.