admin管理员组

文章数量:1122846

In my WPF application (.NET 8.0) I want to consume a asynchronous operation from a 3rd party library. When testing with all code inside the MainWindow.xaml.cs it works smoothely. When I move the same code into a separate class, using a static constructor, the operation is blocking indefinitely even with WaitAsync.

Here is my code:

  public class VirtuosoCommunication
  {
    ..
    public static VirtuosoCommunication? Create(...)
    {
      VirtuosoCommunication vc = new VirtuosoCommunication();
      // setting private variables for vc

      if (!vc.TestRead(log)) return null;
      if (!vc.TestWrite(log)) return null;

      return vc;
    }

    private bool TestRead(Log? log)
    {
      string testGraph = "...";
      Task<Graph?> task = LoadGraphFromSparqlRemoteEndpoint(testGraph, log);
            
      Graph? g = task.Result;
      return g != null;
    }

    internal async Task<Graph?> LoadGraphFromSparqlRemoteEndpoint(string graphFullName, Log? log)
    {
      Uri baseUri = new Uri(_readEndpoint ?? "");

      HttpClient client = new HttpClient();
      client.BaseAddress = baseUri;

      SparqlQueryClient sparqlQueryClient = new SparqlQueryClient(client, baseUri);

      Graph result = new Graph();
      IGraph? tmp = null;

      try
      {
        while (tmp == null || tmp.Triples.Count == limit)
        {
          string query = "construct { ?s ?p ?o } FROM <" + graphFullName + "> where {?s ?p ?o.} OFFSET " + offSet.ToString() + " LIMIT " + limit.ToString();
          Task<IGraph> task = sparqlQueryClient.QueryWithResultGraphAsync(query);
          tmp = await task.WaitAsync(TimeSpan.FromSeconds(4));

          ...
        }
      }
      catch (Exception ex) { ... }

      return result;
    }

When I run the code line by line, it stops executing at tmp = await task.WaitAsync. When I then hit the "Break execution" Button in Visual Studio, it is showing me the line Graph? g = task.Result in the TestRead method to be the next to be executed when the thread returns.

As I said, the same async call in a test routine completely placed in MainWindow.xaml.cs works without problem, so I guess I am somehow using the async call wrongly.

UPDATE 25.11.2024 From the answer of this question I changed the TestRead method to:

private bool TestRead(Log? log)
{
  string testGraph = "...";
  //Task<Graph?> task = LoadGraphFromSparqlRemoteEndpoint(testGraph, log);
  Task<Graph?> task = Task.Run<Graph?>(async() => await LoadGraphFromSparqlRemoteEndpoint(testGraph, log));

        
  Graph? g = task.Result;
  return g != null;
}

It works this way (yeah!), but I am not sure if this is the correct way to do it. Regarding parallelization, I just need the call to the SPARQL endpoint to not block my UI, I don't need to run other, parallel tasks meanwhile.

In my WPF application (.NET 8.0) I want to consume a asynchronous operation from a 3rd party library. When testing with all code inside the MainWindow.xaml.cs it works smoothely. When I move the same code into a separate class, using a static constructor, the operation is blocking indefinitely even with WaitAsync.

Here is my code:

  public class VirtuosoCommunication
  {
    ..
    public static VirtuosoCommunication? Create(...)
    {
      VirtuosoCommunication vc = new VirtuosoCommunication();
      // setting private variables for vc

      if (!vc.TestRead(log)) return null;
      if (!vc.TestWrite(log)) return null;

      return vc;
    }

    private bool TestRead(Log? log)
    {
      string testGraph = "...";
      Task<Graph?> task = LoadGraphFromSparqlRemoteEndpoint(testGraph, log);
            
      Graph? g = task.Result;
      return g != null;
    }

    internal async Task<Graph?> LoadGraphFromSparqlRemoteEndpoint(string graphFullName, Log? log)
    {
      Uri baseUri = new Uri(_readEndpoint ?? "");

      HttpClient client = new HttpClient();
      client.BaseAddress = baseUri;

      SparqlQueryClient sparqlQueryClient = new SparqlQueryClient(client, baseUri);

      Graph result = new Graph();
      IGraph? tmp = null;

      try
      {
        while (tmp == null || tmp.Triples.Count == limit)
        {
          string query = "construct { ?s ?p ?o } FROM <" + graphFullName + "> where {?s ?p ?o.} OFFSET " + offSet.ToString() + " LIMIT " + limit.ToString();
          Task<IGraph> task = sparqlQueryClient.QueryWithResultGraphAsync(query);
          tmp = await task.WaitAsync(TimeSpan.FromSeconds(4));

          ...
        }
      }
      catch (Exception ex) { ... }

      return result;
    }

When I run the code line by line, it stops executing at tmp = await task.WaitAsync. When I then hit the "Break execution" Button in Visual Studio, it is showing me the line Graph? g = task.Result in the TestRead method to be the next to be executed when the thread returns.

As I said, the same async call in a test routine completely placed in MainWindow.xaml.cs works without problem, so I guess I am somehow using the async call wrongly.

UPDATE 25.11.2024 From the answer of this question I changed the TestRead method to:

private bool TestRead(Log? log)
{
  string testGraph = "...";
  //Task<Graph?> task = LoadGraphFromSparqlRemoteEndpoint(testGraph, log);
  Task<Graph?> task = Task.Run<Graph?>(async() => await LoadGraphFromSparqlRemoteEndpoint(testGraph, log));

        
  Graph? g = task.Result;
  return g != null;
}

It works this way (yeah!), but I am not sure if this is the correct way to do it. Regarding parallelization, I just need the call to the SPARQL endpoint to not block my UI, I don't need to run other, parallel tasks meanwhile.

Share Improve this question edited Nov 25, 2024 at 11:20 Aaginor asked Nov 22, 2024 at 11:30 AaginorAaginor 4,78212 gold badges55 silver badges77 bronze badges 12
  • 2 "Graph? g = task.Result;" - just one example where this screams for a deadlock. Await your tasks and go async all the way. – Fildor Commented Nov 22, 2024 at 11:32
  • 2 "using a static constructor" - you are not using a static ctor. This is a factory method. Sounds pedantic but there is a significant enough difference there to point it out. – Fildor Commented Nov 22, 2024 at 11:44
  • 3 this is a classic deadlock caused by .Result; don't do that – Marc Gravell Commented Nov 22, 2024 at 12:00
  • On a side note: QueryWithResultGraphAsync accepts a cancellation token, why don't you just use that for timeouts? – Charlieface Commented Nov 22, 2024 at 12:38
  • 1 @Fildor: Thanks for your replies! I did some more research about async/await (I was just using it because of the 3rd party lib that provides the QueryWithResultGraphAsync-method. Until now, I used the Background-Worker for asynchronous operations) and learned about the context-blocking problem. Also, it is not pedantic to point out my wrong usage of concept names. Especially as programmer it is very mandatory to use exact language, as the machine is not doing what we want, but what we tell it to do. So I am happy to get guidance beyond my question! – Aaginor Commented Nov 25, 2024 at 9:54
 |  Show 7 more comments

2 Answers 2

Reset to default 4

As Fildor said in the comments, you're not using a static cctor; you're using a static factory method.

And that's a good thing. A ctor or cctor cannot be async, so you wouldn't be able to await anything. But a factory method can be async. And in this case, it should be:

public static async Task<VirtuosoCommunication?> Create(...)
{
    VirtuosoCommunication vc = new VirtuosoCommunication();
    // setting private variables for vc

    if (!await vc.TestRead(log)) return null;
    if (!await vc.TestWrite(log)) return null;

    return vc;
}

private async Task<bool> TestRead(Log? log)
{
    string testGraph = "...";
    Graph? g = await LoadGraphFromSparqlRemoteEndpoint(testGraph, log);
    return g != null;
}

Next, I'd suggest looking at your HttpClient usage, which is problematic:

  • You're using HttpClient wrong and it is destabilizing your software | ASP.NET Monsters
  • You're (probably still) using HttpClient wrong and it is destabilizing your software

You should consider using IHttpClientFactory instead of creating a new HttpClient instance on every call.

Use IHttpClientFactory to implement resilient HTTP requests - .NET | Microsoft Learn

Your updated answer using Task.Run() is likely correct, but it is not completely clear without context:

When you're awaiting tasks with await DoSomething(); the current synchronization context is captured (if called from a button click in WPF, this will be the main thread). When the async operation completes, the state machine will schedule the next line of code to be executed in the same context (ie. the main thread). However, since your calling task.Result in TestRead you're already blocking the main thread, resulting in a deadlock.

Calling Task.Run() avoids this problem by making sure everything "inside" the Task.Run block is executed on the thread pool, which does not preserve synchronization context and will not deadlock as it will just spawn new threads if all of them are blocked.

Please note that it would still be better to use async / await all the way through and avoid Task.Result and Task.Wait() as much as possible. If you can change the TestRead function to return a Task instead (like Richard Deeming proposed here https://stackoverflow.com/a/79214898/1074014) definitely go for this approach!

本文标签: casync await blocks execution even with taskWaitAsyncStack Overflow