Michael Tot Korsgaard Michael Tot Korsgaard - 1 month ago 18
C# Question

C# BackgroundWorker return value after finishing

I have this method

public override SockResponse Process(Socket socket, Client client) {
CallResponse rsp = new CallResponse();
try
{
using (Transact X = client.Session.NewTransaction())
{
foreach (CallData call in Payload)
{
DataCall dataCall = new DataCall();
SqlDataReader rdr = dataCall.Execute(X, call);
rsp.Result.Add(dataCall.BuildResult(rdr));
rdr.Close();
}
rsp.ErrMsg = "";
X.Commit();
}
}
catch (Exception err)
{
rsp.ErrMsg = err.Message;
return rsp;
}
return rsp;
}


and now I want to make it work asynchronously, and for that I've tried using
BackgroundWorker
, the new code looks like this

public override SockResponse Process(Socket socket, Client client)
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(
delegate(object sender, DoWorkEventArgs e)
{
CallResponse rsp = new CallResponse();
try
{
using (Transact X = client.Session.NewTransaction())
{
foreach (CallData call in Payload)
{
DataCall dataCall = new DataCall();
SqlDataReader rdr = dataCall.Execute(X, call);
rsp.Result.Add(dataCall.BuildResult(rdr));
rdr.Close();
}
rsp.ErrMsg = "";
X.Commit();
}
}
catch (Exception err)
{
rsp.ErrMsg = err.Message;
e.Result = rsp;
}
e.Result = rsp;
});
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
delegate(object sender, RunWorkerCompletedEventArgs e)
{
// First, handle the case where an exception was thrown.
if (e.Error != null)
{
Log.Logger.Error(e.Error.Message);
}
else if (e.Cancelled)
{
// Next, handle the case where the user canceled
// the operation.
// Note that due to a race condition in
// the DoWork event handler, the Cancelled
// flag may not have been set, even though
// CancelAsync was called.
Log.Logger.Info("CALL process was cancelled");
}
else
{
// Then, handle the result
return e.Result; // this is where the problem is
}
});
worker.RunWorkerAsync();
}


The problem is that I have no idea on how to return the result when the
worker
is done

Answer

It appears you want to run a async code inside a synchronous method. I.e. you have a method which returns a SocketResponse, and want to make it asynchronous.

Well, here's the problem. Basically - you can't. Or at least it's not as simple as just making use of BackgroundWorker.

One way to do this is to also employ a TaskCompletionSource, and then wait for it to finish. The code would look like this:

public override SockResponse Process(Socket socket, Client client)
{
    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += new DoWorkEventHandler(
        delegate (object sender, DoWorkEventArgs e)
        {
            CallResponse rsp = new CallResponse();
            try
            {
                using (Transact X = client.Session.NewTransaction())
                {
                    foreach (CallData call in Payload)
                    {
                        DataCall dataCall = new DataCall();
                        SqlDataReader rdr = dataCall.Execute(X, call);
                        rsp.Result.Add(dataCall.BuildResult(rdr));
                        rdr.Close();
                    }
                    rsp.ErrMsg = "";
                    X.Commit();
                }
            }
            catch (Exception err)
            {
                rsp.ErrMsg = err.Message;
                e.Result = rsp;
            }
            e.Result = rsp;
        });
    TaskCompletionSource<SockResponse> tcs = new TaskCompletionSource<SockResponse>();
    worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
        delegate (object sender, RunWorkerCompletedEventArgs e)
        {
            // First, handle the case where an exception was thrown.
            if (e.Error != null)
            {
                Log.Logger.Error(e.Error.Message);
            }
            else if (e.Cancelled)
            {
                // Next, handle the case where the user canceled 
                // the operation.
                // Note that due to a race condition in 
                // the DoWork event handler, the Cancelled
                // flag may not have been set, even though
                // CancelAsync was called.
                Log.Logger.Info("CALL process was cancelled");
            }
            else
            {
                //  Then, handle the result
                tcs.SetResult(e.Result);
            }
        });
    worker.RunWorkerAsync();
    return tcs.Task.Result;
}

This is an ugly approach - your Process method might be running code in a separate thread, but will otherwise take a long time to execute. The use of BackgroundWorker in this case is also overkill (a simple Task would work just the same in this case). A simplified version of the code using a Task would be:

public override SockResponse Process(Socket socket, Client client)
{
    return Task.Factory.StartNew(() =>
    {
        CallResponse rsp = new CallResponse();
        try
        {
            using (Transact X = client.Session.NewTransaction())
            {
                foreach (CallData call in Payload)
                {
                    DataCall dataCall = new DataCall();
                    SqlDataReader rdr = dataCall.Execute(X, call);
                    rsp.Result.Add(dataCall.BuildResult(rdr));
                    rdr.Close();
                }
                rsp.ErrMsg = "";
                X.Commit();
            }
        }
        catch (Exception err)
        {
            rsp.ErrMsg = err.Message;
            return rsp;
        }
        return rsp;
    }).Result;
}

The above is running code in a new thread, but the Process method will still take a lot of time.

A BETTER approach is to make use of the async/await keywords and rewrite the method to return a Task. This MIGHT require that you rewrite your application in more places than one.

Here's how that same method might look like as an async method:

public async Task<SockResponse> ProcessAsync(Socket socket, Client client)
{
    var task = Task.Factory.StartNew(() =>
    {
        CallResponse rsp = new CallResponse();
        try
        {
            using (Transact X = client.Session.NewTransaction())
            {
                foreach (CallData call in Payload)
                {
                    DataCall dataCall = new DataCall();
                    SqlDataReader rdr = dataCall.Execute(X, call);
                    rsp.Result.Add(dataCall.BuildResult(rdr));
                    rdr.Close();
                }
                rsp.ErrMsg = "";
                X.Commit();
            }
        }
        catch (Exception err)
        {
            rsp.ErrMsg = err.Message;
            return rsp;
        }
        return rsp;
    });
    return await task;
}

HOWEVER, I already see potential issues with this: your Process method is an override, meaning you can't just make it return a Task, and you can't just slap on the async keyword on it either.

So, depending on how you're running the Process method, perhaps you can simply run that in a separate thread? Something along the lines of:

var socketResponse = await Task.Factory.StartNew(() => Process(socket, client));

or (if not using async/await)

var socketResponse = Task.Factory.StartNew(() => Process(socket, client)).Result;

Also, unless you're overriding your own base class (meaning you can rewrite code to make it async), then it seems like you're working with sockets - code related to Socket use is, by default, based on events (Begin/End methods), and should be naturally "asynchronous".

Comments