maegus
maegus

Reputation: 59

Task issue with Powershell and TaskScheduler

I am writing a C# library (.NET 4.6.1) that will use Tasks to be able to run several snippets of code in the background. This library will be called by a Powershell script that is triggered from a TaskScheduler item

When I do testing by launching Powershell, and then calling the library, all is working correctly. However, through the TaskScheduler it does not work, and I can replicate this behavior issue with a console application.

Main library thread:

public class MainLibrary {
private static HttpClient g_httpclient_Main = new HttpClient();
private static List<Task> g_list_Tasks = new List<Task>();

public void EntryPoint() {
 SetupTasks();

 foreach (Task _t_List in g_list_Tasks)
 {
   _t_List.Start();
 }
 Task.WaitAll(g_list_Tasks.ToArray());
}
}

Each of the tasks were created before the foreach loop in a separate function/method of the MainLibrary class like this:

private void SetupTasks() {
  Task _task_TollGate = new Task(() =>
  {
     QueueEndpointCall();
  });
  g_list_Tasks.Add(_task_TollGate);
}


private async Task QueueEndpointCall()
{
   try {
     HttpResponseMessage _httprm = await g_httpclient_Main.GetAsync("http://www.google.com");
     _string_HTTP = await _httprm.Content.ReadAsStringAsync();
     ....other code
   }
   catch (Exception e) {
     File.WriteLine...
   }
}

When I run the console application and put a breakpoint, when the GetAsync is hit and run, it will jump back to the main thread which if I understand correctly is fine/correct. Then hitting the Task.WaitAll line, seemingly the background Task has finished, but the placeholder I have in the snippet "...other code" does not execute. Launching Powershell manually, and calling the library, works fine.

Any suggestions here ? I am considering upgrading to .NET v8.0, however I don't have the resources at this moment to do that and would like to get this working in .NET 4.6.1

Edit (7-Jan-2025) - I migrated to .NET 8 with no success, and even tried the suggestion to locally declare HttpClient although I wouldn't think that's the problem, and still the "...other code" is not being executed. I believe an error is being swallowed as I don't see an error log file from the try/catch that is wrapping the HttpClient call.

Thanks.

Upvotes: 0

Views: 128

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43845

You have made a wrong assumption about what each task in the g_list_Tasks represents. It represents the launching of an asynchronous QueueEndpointCall operation. It doesn't represent the completion of this operation. When a task in the g_list_Tasks completes it means that the QueueEndpointCall has just been launched, not that it has been completed. The way you have defined your tasks, you have deprived yourself from all means to know when the QueueEndpointCall operations complete. There are two solutions to this problem:

  1. Avoid using the Task constructor, and use the Task.Run instead (.NET Framework 4.5+). The Task.Run understands async delegates, and returns a task that represents both the launching and completion of the asynchronous operation. This is the recommended solution.

  2. Keep using the the Task constructor, but use nested Task<Task> instances instead of simple Task. The inner task of this nested structure represents the completion of the asynchronous operation:

    private static List<Task<Task>> g_list_Tasks = new List<Task<Task>>();

    // ...

    Task<Task> _task_TollGate = new Task<Task>(() => QueueEndpointCall());

    // ...

    Task.WaitAll(g_list_Tasks.Select(t => t.Unwrap()).ToArray());

The Unwrap method (.NET Framework 4.0+) unwraps a nested Task<Task> to a simple Task that represents both tasks, the outer and the inner.

As a side note, when you use the Task.Start method your are advised to specify the TaskScheduler parameter, to avoid making your code sensitive to the ambient TaskScheduler.Current. For details see the CA2008 guideline. Correct usage:

_t_List.Start(TaskScheduler.Default);

But you should probably avoid all this complexity and use the Task.Run instead.

Upvotes: 1

maegus
maegus

Reputation: 59

I was able to get this to work correctly:

private readonly static HttpClient g_httpclient_Main = new HttpClient();

public void EntryPoint(string f_string_Metadata)
{
    System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
    List<Task> g_list_Tasks = new List<Task>();
    SetupTasks(g_list_Tasks);

    foreach (Task _t_List in g_list_Tasks)
    {
        _t_List.Start();
    }
    Task.WaitAll(g_list_Tasks.ToArray());
}

private void SetupTasks(List<Task> f_List)
{
    Task _task_TollGate = new Task(() =>
    {
        QueueEndpointCall();
    });
    f_List.Add(_task_TollGate);
}

private void QueueEndpointCall()
{
    Task<string> _t = g_httpclient_Main.GetStringAsync("http://www.google.com");
    Task.WaitAll(_t);
    Console.WriteLine(_t.Result);
}

The GetStringAsync was also similarly to the GetAsync call in my original snippet, seemingly not waiting when the Task.WaitAll was called from the main thread. So I grabbed the Task object from the GetStringAsync call and did a Task.WaitAll on that. Once I did this, then the code flow hit the Console.WriteLine after completing the GetStringAsync operation.

Upvotes: 0

Related Questions