Introduction
In my previous tip / trick, helper class for calling synchronous methods asynchronously, I created a class with all the boiler-plate code needed to run a synchronous function asynchronously using async await
and passing the response back via a delegate to the calling application. It was useful if you were performing a lengthy task which takes (n) seconds to accomplish, and you needed your application to remain responsive.
However, if you needed to run the same function (n) times, there would be no gain as it would always take (n*n) seconds to complete, because the application awaits the result.
In this new helper class, instead of using async await
, I'm passing the Func<T, V>
method to a ThreadPool
, capturing and handling any errors, and again sending the result via a delegate.
Using this class keeps the calling application responsive, whilst being significantly faster. E.g. if a method takes 10 seconds to run and it needs 5 consecutive calls, instead of awaiting 5 responses, thereby taking 50 seconds, this new class will action the 5 calls in just over 10 seconds.
Using the Code
First, here's the code for the new Actor
class.
public class Actor<T, V>
{
public delegate void WhenComplete(object sender, State e);
public virtual void Perform(Func<T, V> job, T parameter, WhenComplete done)
{
ThreadPool.QueueUserWorkItem(new WaitCallback((x) => {
var state = x as State;
state.Run();
done?.Invoke(this, state);
}), new State(job, parameter));
}
public class State
{
public T parameter { get; private set; }
public V result { get; private set; }
private Func<T, V> job;
public Exception error { get; private set; }
public State(Func<T, V> j, T param)
{
job = j;
parameter = param;
}
internal void Run()
{
try
{
if (job == null | parameter == null)
{
throw new NullReferenceException
("A value passed to execute is null.
Check the response to determine the value.");
}
result = job(parameter);
}
catch (Exception e)
{
error = e;
result = default(V);
}
}
}
}
Let's look at how to consume this class in a calling application. On a button click event, I'll create and run a new Actor
class to consume an already defined function and return the result via a delegate. First, I'll need a function to simulate a lengthy task. This function takes a string
as its parameter and returns an int
, the length of the string
parameter.
int LengthyTask(string f)
{
Thread.Sleep(10000);
if (string.IsNullOrWhiteSpace(f))
{
throw new Exception("String is empty.");
}
return f.Length;
}
Next, I'll need a delegate to handle cross thread operations because I'm sending the results to a listbox
on the main thread.
delegate void UpdateCallback(object sender, Actor<string, int>.State state);
Next, my receiver delegate, it has the same signature as the UpdateCallback
delegate.
private void WhenCompleted(object sender, Actor<string, int>.State state)
{
if (listBox1.InvokeRequired)
{
UpdateCallback d = new UpdateCallback(WhenCompleted);
Invoke(d, new object[] { sender, state });
}
else
{
if (state.error != null)
{
listBox1.Items.Add("Exception : " + state.error.Message);
}
else
{
listBox1.Items.Add("Length of " + state.parameter + " is " + state.result);
}
}
}
Looks good so far. Time to add an Actor
class and pass the LengthyTask
method, parameter and receiver delegate, all inside a button click event. When I create my new Actor
class, it should match the signature of the lengthy task, i.e., takes a string (T)
and receives an int (V)
.
Also, the code doesn't need to be wrapped in a try catch
block as all errors are handled and caught by the Actor
class. Therefore, the stack trace isn't lost as it's contained in the state object of the receiver delegate.
private void ButtonClickEvent(object sender, EventArgs e)
{
var actor = new Actor<string, int>();
for (int i = 0; i < 5; i++)
{
var msg = "Task";
if (i % 3 == 0)
{
if (i > 0)
{
msg = null;
}
}
actor.Perform(LengthyTask4, msg, WhenCompleted);
}
}
I hope you find this class helpful. Any feedback is appreciated.
History
- 6th February, 2017: Initial version