Click here to Skip to main content
15,885,216 members
Articles / Programming Languages / C#
Tip/Trick

Pass Generic Func<T,V> to ThreadPool and Wait for Response

Rate me:
Please Sign up or sign in to vote.
4.95/5 (14 votes)
6 Feb 2017CPOL2 min read 14.3K   11   7
A helper class to run func<T,V> in ThreadPool.QueueUserWorkItem and receive the response from a delegate

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.

C++
/// <summary>
/// This Actor class can be used to call a function which has one 
/// parameter, object (T) and returns the result, object (V). The 
/// result is returned in a delegate.
/// </summary>
/// <typeparam name="T">Input type</typeparam>
/// <typeparam name="V">Output type</typeparam>
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 takes an object which represents the data
        // to be used by the queued method in WaitCallback.  I'm using an anonymous 
        // delegate as the method in WaitCallback, and passing the variable state 
        // as the data to use.  When a thread becomes available the method will execute.
        ThreadPool.QueueUserWorkItem(new WaitCallback((x) => {

            var state = x as State;

            state.Run();

            // If the calling application neglected to provide a WhenComplete delegate
            // check if null before attempting to invoke.
            done?.Invoke(this, state);            

        }), new State(job, parameter));
    }

    // Waitcallback requires an object, lets create one.
    public class State
    {
        /// <summary>
        /// This is the parameter which is passed to the function
        /// defined as job.
        /// </summary>
        public T parameter { get; private set; }

        /// <summary>
        /// This will be the response and will be sent back to the 
        /// calling thread using the delegate (a).
        /// </summary>
        public V result { get; private set; }

        /// <summary>
        /// Actual method to run.
        /// </summary>
        private Func<T, V> job;

        /// <summary>
        /// Capture any errors and send back to the calling thread.
        /// </summary>
        public Exception error { get; private set; }

        public State(Func<T, V> j, T param)
        {
            job = j;

            parameter = param;        
        }

        /// <summary>
        ///  Set as an internal types void so only the Actor class can  
        ///  invoke this method.
        /// </summary>
        internal void Run()
        {
            try
            {
                // I think I should check if the method or parameter is null, and react 
                // accordingly.  I can check both values at once and throw a null 
                // reference exception.
                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.

C++
int LengthyTask(string f)
{
    // fake a lengthy task...of 10 seconds
    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.

C++
// delegate to handle cross thread operations.
delegate void UpdateCallback(object sender, Actor<string, int>.State state);

Next, my receiver delegate, it has the same signature as the UpdateCallback delegate.

C++
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
    {
        // check for any errors and handle them.  For this test I'm simply
        // adding the error message to the listbox.
        if (state.error != null)
        {
            listBox1.Items.Add("Exception : " + state.error.Message);
        }
        else
        {
            // any errors can be 
            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.

C++
private void ButtonClickEvent(object sender, EventArgs e)
{
    var actor = new Actor<string, int>();
    
    for (int i = 0; i < 5; i++)
    {
        // a string to analyse. 
        // During the loop reset msg to null to simulate an error being caught.
        var msg = "Task";

        // it's the third task which is designed to fail.
        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionBackgroundWorker Pin
KevinAG9-Feb-17 5:40
KevinAG9-Feb-17 5:40 
PraiseVery nice example Pin
asiwel7-Feb-17 18:24
professionalasiwel7-Feb-17 18:24 
GeneralRe: Very nice example Pin
BrettPerry8-Feb-17 1:12
BrettPerry8-Feb-17 1:12 
QuestionElegant Pin
Michael Breeden7-Feb-17 7:22
Michael Breeden7-Feb-17 7:22 
I'd like to have seen this about 30 months ago

AnswerRe: Elegant Pin
BrettPerry7-Feb-17 8:35
BrettPerry7-Feb-17 8:35 
QuestionSuggestion Pin
Henrique C.7-Feb-17 5:17
professionalHenrique C.7-Feb-17 5:17 
AnswerRe: Suggestion Pin
BrettPerry7-Feb-17 8:34
BrettPerry7-Feb-17 8:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.