简体   繁体   English

C# async/await with HttpClient GetAsync - 非异步调用方法

[英]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.这是从无法标记为异步的主 function 调用时的异步/等待 - function必须同步运行,但由于 HttpClient 是异步的,因此在某些时候,等待必须使事情正常停止。 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.我已经阅读了很多关于.Result.Wait如何导致死锁的内容,但这些是唯一真正使代码同步的版本。

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.这是一个示例程序,大致遵循代码的作用——注意循环中的 4 次尝试——只有其中一次尝试以正确的顺序输出数据。 Is there something fundamentally wrong with this structure/can I not use example #3?这个结构有什么根本性的错误吗/我不能使用示例 #3 吗?

The closest example I can find is here and that is where the calling function can be made async, which this one cannot.我能找到的最接近的例子是在这里,那是调用 function 可以异步的地方,而这个不能。 I've tried making private static void Process() an async and calling it with Task.Run(async ()=> await Process());我试过将private static void Process()异步并使用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.唯一始终如一的工作是Wait/Result ,它可能会死锁,特别是我读过的 HttpClient 。 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.您不想在执行 I/O 时锁定您的主线程,否则您的应用程序在最字面意义上就是垃圾。

If you don't want to make your entire application async -aware for whatever reason, you can still use proper async patterns.如果您出于某种原因不想让整个应用程序具有async感知能力,您仍然可以使用适当的async模式。 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.请记住, await只不过是Task class 已经为您提供的语法糖:一种在任务完成后继续执行的方法。

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.因此,与其让进程 function 锁定主线程, Task.WhenAll .ContinueWith()如果它们是应该并行运行)并立即返回。 The continuation then takes care of updating your UI with the data received (if successful) or error information (if it failed).然后,continuation 负责使用接收到的数据(如果成功)或错误信息(如果失败)更新您的 UI。

the function must run synchronously function 必须同步运行

As others have noted, the best solutions are to go async all the way .正如其他人指出的那样,最好的解决方案是 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.在某些时候, await 必须让事情优雅地停止。

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.我已经阅读了很多有关 .Result 或 .Wait 如何导致死锁的内容,但这些是唯一真正使代码同步的版本。

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.它实际上与ResultWait本身无关——它是任何一种阻塞。 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. await默认情况下确实捕获上下文。
  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.例如,UI 线程上下文或遗留 ASP.NET 请求上下文一次只允许一个线程。

Console applications and ASP.NET Core applications do not have a context, so a deadlock will not happen in those scenarios.控制台应用程序和 ASP.NET 核心应用程序没有上下文,因此在这些场景中不会发生死锁。

The only thing that consistently works is Wait/Result which can deadlock... Any thoughts?唯一始终如一的工作是 Wait/Result ,它可以死锁......有什么想法吗?

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).这适用于 Console / ASP.NET Core 应用程序(因为它们没有会导致死锁的上下文)。 I recommend using GetAwaiter().GetResult() in this case to avoid exception wrappers that come when using Result / Wait() :我建议在这种情况下使用GetAwaiter().GetResult()以避免在使用Result / Wait()时出现异常包装器:

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

However, that approach will not work if the application is a UI app or legacy ASP.NET app.但是,如果应用程序是 UI 应用程序或旧版 ASP.NET 应用程序,则该方法将不起作用。 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:线程池线程在上下文之外运行,因此它避免了死锁,但是这个 hack 意味着CallAsyncTest必须能够在线程池线程上运行 - 不能访问 UI 元素或HttpContext.Current或任何其他依赖于上下文的东西,因为上下文不会在那里:

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.如果代码可以在任意线程池线程上运行,则在Task.Run上阻塞会起作用。 There are a few other hacks available but those two are the most common.还有一些其他的 hack 可用,但这两个是最常见的。

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". “Try 4”与“Try 1”完全相同。 The call to ConfigureAwait does nothing because there is no await to configure.ConfigureAwait的调用不执行任何操作,因为没有await配置。 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?为什么不使 Process 方法异步,然后使用正常的异步等待模式?

The Process signature would be Process 签名将是

private static async Task Process()

That would allow you to use await CallAsyncTest(i)这将允许您使用await CallAsyncTest(i)

and you would Wait it on the main method, like so:你会在 main 方法上等待它,就像这样:

Process.GetAwaiter().GetResult();

This would be the equivalent to the implementation of async main introduced in C# 7.1这相当于 C# 7.1 中引入的 async main 的实现

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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