ChatGPT解决这个技术问题 Extra ChatGPT

从非异步代码调用异步方法

我正在更新具有在 .NET 3.5 中构建的 API 表面的库。因此,所有方法都是同步的。我无法更改 API(即,将返回值转换为任务),因为这需要所有调用者都更改。所以我只剩下如何以同步方式最好地调用异步方法了。这是在 ASP.NET 4、ASP.NET Core 和 .NET/.NET Core 控制台应用程序的上下文中。

我可能还不够清楚 - 情况是我有不支持异步的现有代码,我想使用新的库,例如 System.Net.Http 和仅支持异步方法的 AWS 开发工具包。所以我需要弥合差距,并且能够拥有可以同步调用但随后可以在其他地方调用异步方法的代码。

我做了很多阅读,并且有很多次被问和回答。

Calling async method from non async method

Synchronously waiting for an async operation, and why does Wait() freeze the program here

Calling an async method from a synchronous method

How would I run an async Task<T> method synchronously?

Calling async method synchronously

How to call asynchronous method from synchronous method in C#?

问题是大多数答案都不一样!我见过的最常见的方法是使用 .Result,但这可能会死锁。我已经尝试了以下所有方法,它们都有效,但我不确定哪种方法是避免死锁、具有良好性能以及与运行时良好配合(在尊重任务调度程序、任务创建选项等方面)的最佳方法)。有确定的答案吗?最好的方法是什么?

private static T taskSyncRunner<T>(Func<Task<T>> task)
    {
        T result;
        // approach 1
        result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 2
        result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 3
        result = task().ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 4
        result = Task.Run(task).Result;

        // approach 5
        result = Task.Run(task).GetAwaiter().GetResult();


        // approach 6
        var t = task();
        t.RunSynchronously();
        result = t.Result;

        // approach 7
        var t1 = task();
        Task.WaitAll(t1);
        result = t1.Result;

        // approach 8?

        return result;
    }
答案是你不这样做。您添加了异步的新方法,并为遗留调用者保留旧的同步方法。
这似乎有点苛刻,并且真的扼杀了使用新代码的能力。例如,新版本的 AWS 开发工具包没有非异步方法。许多其他第三方库也是如此。所以除非你重写世界,否则你不能使用其中任何一个?
选项 8:也许 TaskCompletionSource 可能是一个选项?
Result 属性仅在 Task 仍在运行时调用时会导致死锁。我不是 100% 肯定,但如果你正确地等到任务结束,你应该是安全的,就像方法 7 一样。
展示为什么不能从异步代码中使用同步 API 的示例可能有助于找到比 @scott 更好的答案

u
usr

所以我只剩下如何以同步方式最好地调用异步方法了。

首先,这是一件好事。我之所以这么说是因为在 Stack Overflow 上将其指出为魔鬼的行为是一种笼统的陈述,而不考虑具体情况是很常见的。

为了正确性,不需要一直异步。阻塞异步以使其同步具有可能重要或可能完全不相关的性能成本。这取决于具体情况。

死锁来自试图同时进入同一个单线程同步上下文的两个线程。任何避免这种情况的技术都可以可靠地避免由阻塞引起的死锁。

在您的代码段中,对 .ConfigureAwait(false) 的所有调用都毫无意义,因为不等待返回值。 ConfigureAwait 返回一个结构,该结构在等待时显示您请求的行为。如果该结构被简单地删除,它什么也不做。

RunSynchronously 无法使用,因为并非所有任务都可以这样处理。此方法适用于基于 CPU 的任务,在某些情况下可能无法正常工作。

.GetAwaiter().GetResult()Result/Wait() 的不同之处在于它模仿 await 异常传播行为。您需要决定是否要这样做。 (所以请研究一下该行为是什么;无需在此重复。)如果您的任务包含单个异常,则 await 错误行为通常很方便并且几乎没有缺点。如果有多个异常,例如来自多个任务失败的失败 Parallel 循环,则 await 将删除除第一个异常之外的所有异常。这使得调试更加困难。

所有这些方法都具有相似的性能。他们将以一种或另一种方式分配操作系统事件并阻止它。那是昂贵的部分。与此相比,其他机器相当便宜。我不知道哪种方法绝对最便宜。

如果抛出异常,那将是最昂贵的部分。在 .NET 5 上,在快速 CPU 上以每秒最多 200,000 个的速率处理异常。深堆栈速度较慢,并且任务机制倾向于重新抛出异常,使其成本倍增。有一些方法可以在不重新抛出异常的情况下阻止任务,例如 task.ContinueWith(_ => { }, TaskContinuationOptions.ExecuteSynchronously).Wait();

我个人喜欢 Task.Run(() => DoSomethingAsync()).Wait(); 模式,因为它绝对避免了死锁,它很简单,并且不会隐藏一些 GetResult() 可能隐藏的异常。但是您也可以使用 GetResult()


谢谢,这很有意义。
您也可以将其写为 Task.Run(async () => await DoSomethingAsync()),但这真的重要吗,因为无论如何只有 1 行代码?如果需要,DoSomethingAsync() 中的代码仍然可以使用 await,对吗?
@void.pointer 在语义上,它们是等效的,除了 await 将删除除第一个异常之外的所有异常。否则,您可以使用您喜欢的样式。我可以看到两种风格的论据。 DoSomethingAsync 在内部所做的是与外部屏蔽的。它可以使用它喜欢的任何机制来生成 Taskawait 会这样做,但 Task.FromResult 和其他人也会这样做。
S
Stephen Cleary

我正在更新具有在 .NET 3.5 中构建的 API 表面的库。因此,所有方法都是同步的。我无法更改 API(即,将返回值转换为任务),因为这需要所有调用者都更改。所以我只剩下如何以同步方式最好地调用异步方法了。

没有通用的“最佳”方式来执行同步异步反模式。只有各种各样的黑客,每个都有自己的缺点。

我建议您保留旧的同步 API,然后在它们旁边引入异步 API。您可以使用 "boolean argument hack" as described in my MSDN article on Brownfield Async 执行此操作。

首先,简要说明您的示例中每种方法的问题:

ConfigureAwait 仅在等待时才有意义;否则,它什么也不做。结果会将异常包装在 AggregateException 中;如果您必须阻止,请改用 GetAwaiter().GetResult()。 Task.Run 将在线程池线程上执行其代码(显然)。仅当代码可以在线程池线程上运行时才可以。 RunSynchronously 是一种高级 API,在执行基于任务的动态并行处理时会在极为罕见的情况下使用。你根本不在那种情况下。带有单个任务的 Task.WaitAll 与 Wait() 相同。 async () => await x 只是表示 () => x 的一种效率较低的方式。阻塞从当前线程启动的任务会导致死锁。

这是细分:

// Problems (1), (3), (6)
result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (3)
result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (7)
result = task().ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (2), (3)
result = Task.Run(task).Result;

// Problems (3)
result = Task.Run(task).GetAwaiter().GetResult();

// Problems (2), (4)
var t = task();
t.RunSynchronously();
result = t.Result;

// Problems (2), (5)
var t1 = task();
Task.WaitAll(t1);
result = t1.Result;

除了这些方法中的任何一种,由于您有现有的、正在工作的同步代码,您应该将它与较新的自然异步代码一起使用。例如,如果您的现有代码使用 WebClient

public string Get()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

并且你想添加一个异步 API,那么我会这样做:

private async Task<string> GetCoreAsync(bool sync)
{
  using (var client = new WebClient())
  {
    return sync ?
        client.DownloadString(...) :
        await client.DownloadStringTaskAsync(...);
  }
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

或者,如果您出于某种原因必须使用 HttpClient

private string GetCoreSync()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

private static HttpClient HttpClient { get; } = ...;

private async Task<string> GetCoreAsync(bool sync)
{
  return sync ?
      GetCoreSync() :
      await HttpClient.GetString(...);
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

使用这种方法,您的逻辑将进入 Core 方法,这些方法可以同步或异步运行(由 sync 参数确定)。如果 synctrue,那么核心方法必须返回一个已经完成的任务。实现上,使用同步 API 同步运行,使用异步 API 异步运行。

最后,我建议弃用同步 API。


你能解释一下第六条吗?
@EmersonSoares:正如我在 async intro 中解释的那样,您可以 await 方法的结果,因为它返回 Task,而不是因为它是 async。这意味着for trivial methods, you can drop the keywords
我最近在 asp.net mvc 5 + EF6 中遇到了同样的问题。我使用 TaskFactory 作为这个答案建议 stackoverflow.com/a/25097498/1683040,它对我有用:),但不确定其他场景。
我理解您使用 WebClient 提出的建议。但是使用 HttpClient 时,Core 方法有什么好处呢?如果同步方法直接调用 GetCoreSync 方法,在这种情况下不使用布尔参数,那不是更干净更高效吗?
@Creepin:是的,我相信。
d
dlchambers

我刚刚使用 AWS S3 SDK 完成了这件事。以前是同步的,我在上面写了一堆代码,但现在它是异步的。这很好:他们改变了它,抱怨它没有任何好处,继续前进。
所以我需要更新我的应用程序,我的选择是要么将我的应用程序的大部分重构为异步,要么“破解” S3 异步 API 使其表现得像同步。
我最终会进行更大的异步重构 - 有很多好处 - 但今天我有更大的鱼要炒,所以我选择伪造同步。< br>
原始同步代码是
ListObjectsResponse response = api.ListObjects(request);
一个非常简单的异步等效对我有用
Task<ListObjectsV2Response> task = api.ListObjectsV2Async(rq2);
ListObjectsV2Response rsp2 = task.GetAwaiter().GetResult();

虽然我知道纯粹主义者可能会为此嘲笑我,但现实情况是,这只是众多紧迫问题之一,而且我的时间有限,因此我需要做出权衡。完美的?不,有效吗?是的。


S
Sam pandya

您可以从非异步方法调用异步方法。检查下面的代码。

     public ActionResult Test()
     {
        TestClass result = Task.Run(async () => await GetNumbers()).GetAwaiter().GetResult();
        return PartialView(result);
     }

    public async Task<TestClass> GetNumbers()
    {
        TestClass obj = new TestClass();
        HttpResponseMessage response = await APICallHelper.GetData(Functions.API_Call_Url.GetCommonNumbers);
        if (response.IsSuccessStatusCode)
        {
            var result = response.Content.ReadAsStringAsync().Result;
            obj = JsonConvert.DeserializeObject<TestClass>(result);
        }
        return obj;
    }