简体   繁体   中英

C# async/await with HttpClient GetAsync - non-async calling method

Here is an async/await when calling from a main function which cannot be marked async - the function must run synchronously, but because HttpClient is async, at some point, the await has to make things stop gracefully. I've read a lot about how .Result or .Wait can cause deadlocks, but those are the only versions that actually make the code synchronous.

Here is an example program, roughly following what the code does - note the 4 attempts in the loop - only one of them actually puts data out in the correct order. Is there something fundamentally wrong with this structure/can I not use example #3?

The closest example I can find is here and that is where the calling function can be made async, which this one cannot. I've tried making private static void Process() an async and calling it with Task.Run(async ()=> await Process()); but it still runs out of order. The only thing that consistently works is Wait/Result which can deadlock, particularly with HttpClient from what I've read. Any thoughts?

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace TestAsync
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("1. Calling process at {0:mm.ss.fff}", DateTime.Now);

            // Call 1
            Process();

            Console.WriteLine("1. Calling process at {0:mm.ss.fff}", DateTime.Now);

            // Call 2
            Process();

            Console.ReadKey();
        }

        private static void Process()
        {
            Console.WriteLine("2. Calling CallAsyncTest at {0:mm.ss.fff}", DateTime.Now);

            for (int i = 1; i < 4; i++)
            {
                // Try 1 - doesn't work
                //CallAsyncTest(i);

                // Try 2 - doesn't work
                //Task.Run(async () => await CallAsyncTest(i));

                // Try 3 - but works not recommended
                CallAsyncTest(i).Wait();

                // Try 4 - doesn't work
                //CallAsyncTest(i).ConfigureAwait(false); ;
            }
        }

        private static async Task CallAsyncTest(int i)
        {
            Console.WriteLine("{0}. Calling await AsyncTest.Start at {1:mm.ss.fff}", i + 2, DateTime.Now);

            var x = await AsyncTest.Start(i);
        }
    }

    public class AsyncTest
    {
        public static async Task<string> Start(int i)
        {
            Console.WriteLine("{0}. Calling await Post<string> at {1:mm.ss.fff}", i + 3, DateTime.Now);

            return await Post<string>(i);
        }

        private static async Task<T> Post<T>(int i)
        {
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            using (HttpClient httpClient = new HttpClient(new HttpClientHandler()))
            {
                using (HttpResponseMessage response = await httpClient.GetAsync("https://www.google.com"))
                {
                    using (HttpContent content = response.Content)
                    {
                        string responseString = await content.ReadAsStringAsync();

            

        Console.WriteLine("response");
                }
            }

        }

        return default(T);
    }
}

but those are the only versions that actually make the code synchronous

Exactly, you shouldn't use it for that reason alone. You don't want to lock your main thread while you perform I/O or your application is in the most literal sense trash.

If you don't want to make your entire application async -aware for whatever reason, you can still use proper async patterns. Remember that await is nothing but syntactic sugar around what the Task class already provides you: a way to continue after a task is complete.

So instead of having your process function lock your main thread, you can set up a daisy chain of .ContinueWith() on your four calls (if they're supposed to run in sequence) or on a Task.WhenAll (if they're supposed to run in parallel) and return immediately. The continuation then takes care of updating your UI with the data received (if successful) or error information (if it failed).

the function must run synchronously

As others have noted, the best solutions are to go async all the way . But I'll assume that there's a good reason why this isn't possible.

at some point, the await has to make things stop gracefully.

At some point, your code will have to block on the asynchronous code, yes.

I've read a lot about how.Result or.Wait can cause deadlocks, but those are the only versions that actually make the code synchronous.

Right. Blocking on asynchronous code is the only way to make it synchronous.

It's not actually about Result or Wait per se - it's any kind of blocking. And there are some situations where blocking on asynchronous code will cause deadlocks . Specifically, when all of these conditions are true:

  1. The asynchronous code captures a context . await does capture contexts by default.
  2. The calling code blocks a thread in that context.
  3. The context only allows one thread at a time. Eg, UI thread contexts or legacy ASP.NET request contexts only allow one thread at a time.

Console applications and ASP.NET Core applications do not have a context, so a deadlock will not happen in those scenarios.

The only thing that consistently works is Wait/Result which can deadlock... Any thoughts?

There are some hacks you can choose from to block on asynchronous code.

One of them is just to block directly. This works fine for Console / ASP.NET Core applications (because they don't have a context that would cause the deadlock). I recommend using GetAwaiter().GetResult() in this case to avoid exception wrappers that come when using Result / Wait() :

CallAsyncTest(i).GetAwaiter().GetResult();

However, that approach will not work if the application is a UI app or legacy ASP.NET app. In that case, you can push the asynchronous work off to a thread pool thread and block on that . The thread pool thread runs outside the context and so it avoids the deadlock, but this hack means that CallAsyncTest must be able to be run on a thread pool thread - no accessing UI elements or HttpContext.Current or anything else that depends on the context, because the context won't be there:

Task.Run(() => CallAsyncTest(i)).GetAwaiter().GetResult();

There is no solution that works in every scenario. Blocking directly works if there isn't a context (eg, Console apps). Blocking on Task.Run works if the code can run on an arbitrary thread pool thread. There are a few other hacks available but those two are the most common.

Here's why the other attempts failed.

This one starts the task but doesn't wait at all:

// Try 1 - doesn't work
CallAsyncTest(i);

This one starts the task in a thread pool thread but doesn't wait at all:

// Try 2 - doesn't work
Task.Run(async () => await CallAsyncTest(i));

"Try 4" is exactly the same as "Try 1". The call to ConfigureAwait does nothing because there is no await to configure. So it also just starts the task but doesn't wait at all:

// Try 4 - doesn't work
CallAsyncTest(i).ConfigureAwait(false);

Why not making the Process method async, and then use the normal async await pattern?

The Process signature would be

private static async Task Process()

That would allow you to use await CallAsyncTest(i)

and you would Wait it on the main method, like so:

Process.GetAwaiter().GetResult();

This would be the equivalent to the implementation of async main introduced in C# 7.1

https://learn.microsoft.com/en-us/do.net/csharp/language-reference/proposals/csharp-7.1/async-main#detailed-design

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM