Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected behavior from 2-level Task.ContinueWith

I have some code that lays out like this:

Class1

Task<List<ConfSession>> getSessionsTask = Task.Factory.StartNew(() =>
            {
                var confSessions = 
                    TaskHelper<ConfSession>.InvokeTaskMagic(request);
                //PROBLEM - THIS CODE RACES TO THE NEXT LINE 
                //BEFORE confSessions IS POPULATED FROM THE LINE ABOVE - IE 
                //confSessions IS ALWAYS AN EMPTY LIST 
                //WHEN IT'S RETURNED
                return confSessions;
            }
        );

Class2 (TaskHelper)

//methods
public static List<T> InvokeTaskMagic(HttpWebRequest request)
{
    var resultList = new List<T>();

    Task<WebResponse> task1 = Task<WebResponse>.Factory.FromAsync(
        (callback, o) => ((HttpWebRequest)o).BeginGetResponse(callback, o)
        , result => ((HttpWebRequest)result.AsyncState).EndGetResponse(result)
        , request);

    task1.ContinueWith((antecedent) =>
    {
        if (antecedent.IsFaulted)
        {
            return;
        }

        WebResponse webResponse;
        try
        {
            webResponse = task1.Result;
        }
        catch (AggregateException ex1)
        {
            throw ex1.InnerException;
        }

        string responseString;

        using (var response = (HttpWebResponse)webResponse)
        {
            using (Stream streamResponse = response.GetResponseStream())
            {
                StreamReader reader = new StreamReader(streamResponse);
                responseString = reader.ReadToEnd();
                reader.Close();
            }
        }

        if (responseString != null)
        {
            resultList = 
               JsonConvert.DeserializeObject<List<T>>(responseString);
        }
    });

    return resultList;
}

TaskHelper is a class I created to standardize a bunch of redundant task code that I had in several methods. It exists only to take an HttpWebRequest, execute it in a Task, get the response in the ContinueWith block, parse the response into a List<T>, and return the List<T>.

I wrapped Class1's call to TaskHelper in a task because I need to get the result from the InvokeTaskMagic method before I continue in class1 (i.e. the next step in class1 is to use the List<T>. As it says in the comments in the code, my problem is that the Task in class1 returns an empty list every time because it's not waiting for the response from the InvokeTaskMagic method in the TaskHelper class.

Is this expected? Is there a way to make getSessionsTask wait to return until TaskHelper.InvokeTaskMagic returns?

UPDATE: The working code follows - thanks Servy for your help.

public static class TaskHelper<T> where T : class
{
    //methods
    public static Task<List<T>> InvokeTaskMagic(HttpWebRequest request)
    {
        var resultList = new List<T>();

        Task<WebResponse> task1 = Task<WebResponse>.Factory.FromAsync(
            (callback, o) => ((HttpWebRequest)o).BeginGetResponse(callback, o)
            , result => ((HttpWebRequest)result.AsyncState).EndGetResponse(result)
            , request);

        return task1.ContinueWith<List<T>>((antecedent) =>
        {
            if (antecedent.IsFaulted)
            {
                return new List<T>();
            } 

            WebResponse webResponse;
            try
            {
                webResponse = task1.Result;
            }
            catch (AggregateException ex1)
            {
                throw ex1.InnerException;
            }

            string responseString;

            using (var response = (HttpWebResponse)webResponse)
            {
                using (Stream streamResponse = response.GetResponseStream())
                {
                    StreamReader reader = new StreamReader(streamResponse);
                    responseString = reader.ReadToEnd();
                    reader.Close();
                }
            }

            if (responseString != null)
            {
                return JsonConvert.DeserializeObject<List<T>>(responseString);
            }
            else
            {
                return new List<T>();
            }
        }); 
    }
}

The InvokeTaskMagic method is called like this:

var getCommentsAboutTask = Task.Factory.StartNew(() =>
            {
                var comments = TaskHelper<Comment>.InvokeTaskMagic(request);
                return comments;
            });

        getCommentsAboutTask.ContinueWith((antecedent) =>
            {
                if (antecedent.IsFaulted)
                { return; }

                var commentList = antecedent.Result.Result;
                UIThread.Invoke(() =>
                {
                    foreach (Comment c in commentList)
                    {
                        AllComments.Add(c);
                        GetCommentRelatedInfo(c);
                    }
                });
            });
like image 423
Andrew B Schultz Avatar asked Jan 14 '23 17:01

Andrew B Schultz


2 Answers

Is this expected? Is there a way to make my class1.task1 continueblock wait for my class2.task2 continueblock?

Sure, just call ContinueWith on the second task's continuation instead of on the first task.

If you need it to wait until both are done you can use Task.Factory.ContinueWhenAll.

like image 59
Servy Avatar answered Jan 21 '23 08:01

Servy


I have done something similar to your code some time ago and it is difficult getting it to work properly with error handling etc.

First I created a generic method to handles continuations with different return type

private AsyncCallback EndAsync<T1, T2>(TaskCompletionSource<T2> tcs,
                                       Func<IAsyncResult, T1> endMethod, 
                                       Func<T1, T2> continueWith) {
    return ar => {
        T1 result1;
        try {
            result1 = endMethod(ar);
        }
        catch (Exception err) {
            tcs.SetException(err);
            return;
        }

        try {
            T2 result2 = continueWith(result1);
            tcs.SetResult(result2);
        }
        catch (Exception err) {
            tcs.SetException(err);
            return;
        }
    };
}

Then I create the async task like this:

public Task<List<T>> GetDataAsync(IProxy proxy) {
    var tcs = new TaskCompletionSource<List<T>>();
    var asyncCallback = EndAsync(tcs, 
                                 proxy.EndGetData, 
                                 result => result != null ? 
                                           ProcessResult(result) : 
                                           new List<T>());
    proxy.BeginGetData(asyncCallback);

    return tcs.Task;
}
like image 23
adrianm Avatar answered Jan 21 '23 09:01

adrianm