【问题标题】:C# Async and Sync functions confuse meC# Async 和 Sync 函数让我感到困惑
【发布时间】:2021-11-02 18:23:56
【问题描述】:

我有一个应用程序,其中一个按钮开始创建 XML。在 每个 XML 创建结束时,SendInvoice 函数发送它,接收响应,然后一个函数 (ParseResponse) 解析响应并执行需要数据库操作。

这个想法是,当所有的 XML 被创建和发送后,应用程序必须关闭。 问题是我失去了对异步的控制,并且应用程序似乎在 它实际上完成所有工作之前关闭。此外,在处理之前的 XML 之前发送 XML。

ParseResponse 函数不是异步的。

这里是 SendInvoice 函数。

你能推荐一些好的做法吗?

提前谢谢你。

public async void SendInvoice(string body)
    {
        Cursor.Current = Cursors.WaitCursor;

        var client = new HttpClient();

        var queryString = HttpUtility.ParseQueryString(string.Empty);

        var uri = "https://xxxx.xxx/SendInvoices?" + queryString; 

        HttpResponseMessage response;

        // Request body
        byte[] byteData = Encoding.UTF8.GetBytes(body);

        using (var content = new ByteArrayContent(byteData))
        {
            content.Headers.ContentType = new MediaTypeHeaderValue("application/xml");
            response = await client.PostAsync(uri, content);
            string responsebody = await response.Content.ReadAsStringAsync();

            ParseResponse(response.ToString());
            ParseResponse(responsebody);

        }
    }

剩下的代码

private void button1_Click(object sender, EventArgs e)
{
 For
  {
     ......
      SendInvoice(xml)

  }
  System.Windows.Forms.Application.Exit();
}

【问题讨论】:

  • 1. async void 除非在事件处理程序中使用,否则应避免使用,将方法签名更改为 async Task。 2.您没有发布足够多的代码来确定程序在作业完成之前退出的原因,但我猜这是因为您没有等待作业完成。 IE。 我认为 ParseResponse 的代码和按钮不会提供更多提示。 - 是的。
  • 你可以等待所有作业完成,比如WhenAll()。
  • SendInvoice 将在完成之前返回。您需要在调用 SendInvoice 的地方使用 await,或使 SendInvoice 同步
  • 查看此处了解重构代码的方法stackoverflow.com/a/9343733/2030565
  • @PanosPlat 您可以从同步方法调用异步代码(带有警告)。只需将.Result 附加到调用以等待任务完成并给出返回值

标签: c# asynchronous httpclient


【解决方案1】:

由于您是从事件处理程序调用方法,这是可以接受async void 的情况,请将您的按钮单击处理程序方法签名更改为使用async,我还在异步方法调用中添加了一些ConfigureAwait(false) - best-practice-to-call-configureawait-for-all-server-side-codewhy-is-writing-configureawaitfalse-on-every-line-with-await-always-recommended:

private async void button1_Click(object sender, EventArgs e)
{
  //since you are using a for-loop, I'd suggest adding each Task
  //to a List and awaiting all Tasks to complete using .WhenAll()
  var tasks = new List<Task>();
  
  FOR
  {
     ......
      //await SendInvoice(xml).ConfigureAwait(false);
      tasks.Add(SendInvoice(xml));

  }
  await Task.WhenAll(tasks).ConfigureAwait(false);
  System.Windows.Forms.Application.Exit();

}

并更改您的 SendInvoice 方法签名以返回 Task

public async Task SendInvoice(string body)
{
    Cursor.Current = Cursors.WaitCursor;

    var client = new HttpClient();

    var queryString = HttpUtility.ParseQueryString(string.Empty);

    var uri = "https://xxxx.xxx/SendInvoices?" + queryString; 

    HttpResponseMessage response;

    // Request body
    byte[] byteData = Encoding.UTF8.GetBytes(body);

    using (var content = new ByteArrayContent(byteData))
    {
        content.Headers.ContentType = new MediaTypeHeaderValue("application/xml");
        response = await client.PostAsync(uri, content).ConfigureAwait(false);
        string responsebody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

        ParseResponse(response.ToString());
        ParseResponse(responsebody);

   }
}

【讨论】:

  • 为了清楚起见,对于 WinForms 和 ASP.net 进程(可能还有其他进程),默认情况下任务设置为需要 SAME 线程才能恢复。该线程通常不可用,并且可能正在等待任务本身,从而产生死锁。 ConfigureAwait(false) 配置任务,使其可以使用不同的线程恢复,避免死锁问题。
【解决方案2】:

我非常习惯多线程编程,我花了一些时间来理解异步编程,因为它真的与多线程无关。它是关于使用单个线程或少量线程执行更多操作。

当 CPU 会等待处理之外的其他内容时,异步代码是有益的。例如:等待网络响应、等待从磁盘读取数据、等待单独的进程(例如数据库服务器)。

它为正在运行的线程提供了一种在等待时执行其他操作的方法。 C# 使用Task 执行此操作。任务是一些正在完成的工作,它可以运行也可以等待,等待时不需要附加线程。

所有异步函数都必须返回一个 Task 才能有用。所以你的功能应该是:

public async Task SendInvoice() {
...

编译器使用async 关键字自动将您的函数包装在任务对象中,因此您无需担心很多细节。你只需在调用另一个异步函数时使用await。您可以自己做更多的工作来创建任务或从另一个异步函数返回任务,甚至调用多个异步函数并一起等待它们。

如果您的异步方法返回一个值,请使用通用任务:例如Task&lt;String&gt;

Task 在任务完成之前从异步方法返回。这就是允许线程被其他东西使用的原因,但它必须回到那个起始位置,这就是为什么异步编程你会听到“一直异步”的原因。直到它返回到需要平衡多个任务的调用者(通常是您的应用程序或 Web 请求的入口点)之前,它并没有真正起到任何作用。

您可以使您的 C# Main 方法异步,但除非您的进程确实同时执行多项操作,否则这通常无关紧要。对于 Web 应用程序,它可以只处理多个请求。对于一个独立的应用程序,这意味着您可以查询多个 API,同时发出多个 Web 请求或 db 查询,并等待它们,只使用一个线程。显然,这可以使事情变得更快(至少在本地,外部资源可能有更多工作要做)。

对于防止程序退出的简单方法,如果您有异步 main,只需 await 调用 SendInvoice。如果你的 main 不是异步的,你可以使用类似的东西:

SendInvoice().Wait()

SendInvoice().Result

使用Wait()Result 将锁定线程,直到任务完成。它通常会使该线程专供该任务使用,因此该线程不能用于任何其他任务。如果线程池中有更多线程,其他任务可能会继续运行,但通常在单个任务上使用 Wait/Result 会破坏异步编程的意义,因此请记住这一点。

编辑 现在您已经发布了调用代码,看来您的调用处于循环中。这是利用异步调用并一次发送所有发票的好机会。

private async void button1_Click(object sender, EventArgs e)
{
  List<Task> tasks = new List<Task>();
  FOR
  {
     ......
     t = SendInvoice(xml).ConfigureAwait(false);
     tasks.Add(t)
  }

  await Task.WhenAll(tasks).ConfigureAwait(false);
  System.Windows.Forms.Application.Exit();

}

这将发送所有发票,然后从处理程序返回,然后在收到所有响应后退出。

【讨论】:

    猜你喜欢
    • 2016-09-19
    • 2010-12-24
    • 1970-01-01
    • 1970-01-01
    • 2014-04-26
    • 1970-01-01
    • 1970-01-01
    • 2020-10-15
    • 1970-01-01
    相关资源
    最近更新 更多