【问题标题】:My console app shutdown prematurely when using async / await?使用异步/等待时我的控制台应用程序过早关闭?
【发布时间】:2019-07-22 22:21:36
【问题描述】:

我有一个控制台应用程序,它所做的只是遍历所有客户并向特定客户发送电子邮件,然后关闭。我注意到 MailKit 中的一些功能提供异步,所以我尝试使用它们而不是非 aysnc,当我这样做时,它执行了第一条语句 (emailClient.ConnectAsync),然后我注意到我的控制台应用程序正在关闭。它没有崩溃。在我调用我的 SendReports() 函数后,执行返回到 Main() 并继续执行。

private static void Main(string[] args)
{
  ...
  var reportServices = new ReportsServices();

  reportServices.SendReportsToCustomers();

  Log.CloseAndFlush();  // It executes the first await call then returns here.
}

internal class ReportsServices
{
  public async void SendReportsToCustomers()
  {
    try
    {
      foreach (var customer in _dbContext.Customer)
      {
          ...
          await SendReport(customer.Id, repTypeId, freqName);
      }
    }
    catch (Exception e)
    {
      Console.WriteLine(e);
      throw;
    }
  }

  private async Task SendReport(int customerId, int repTypeId, string freqName)
  {
    try
    {
      ...
        var es = new EmailSender();
        await es.SendAsync();
      }
    }
    catch (Exception e)
    {
      Console.WriteLine(e);
      throw;
    }

  }
}

public class EmailSender
{
  public async Task SendAsync()
  {
    try
    {
      var message = new MimeMessage();

      ...

      using (var emailClient = new SmtpClient())
      {
        await emailClient.ConnectAsync("smtp.gmail.net", 587);  
        await emailClient.AuthenticateAsync("username", "password"); // If I put a debug break here, it doesn't hit.
        await emailClient.SendAsync(message);
        await emailClient.DisconnectAsync(true);

       // If I use the following calls instead, my console app will not shutdown until all the customers are sent emails.

await emailClient.Connect("smtp.gmail.net", 587);  
await emailClient.Authenticate("username", "password"); // If I put a debug break here, it doesn't hit.
await emailClient.Send(message);
await emailClient.Disconnect(true);

      }
    }
    catch (Exception e)
    {
      Console.WriteLine(e);
      throw;
    }

  }
}

我不明白为什么我的循环不能继续遍历所有客户? - 还有更多工作要做。不知道为什么它会跳回 Main 函数。

我所希望的是循环将继续通过所有客户并发送电子邮件;无需等待电子邮件发送完毕即可继续下一封邮件。

感谢您的帮助!

【问题讨论】:

  • await 是非阻塞的。调用线程继续并行运行,而不是方法的其余部分。无论调用SendReportsToCustomers 都不能在其上使用await,因为它不会返回Task。如果您想要阻止使用Task.Result
  • I am still trying to get my head around this 我建议从我的async intro 开始,然后是我的async best practices

标签: c# multithreading mailkit


【解决方案1】:

正如其他人所解释的,控制台应用程序不会等待异步方法完成。它将直接跳转到下一行代码。如果它到达您的 main 函数的末尾,控制台应用程序将结束。

也许您可以在 Main 函数的末尾添加 Console.ReadLine() 以防止控制台应用突然结束。

【讨论】:

    【解决方案2】:

    您似乎不明白await 的工作原理。每次在函数上调用await 时,调用者函数本身都会暂时返回,直到等待的函数返回。

    这意味着,除非等待的函数及时返回,否则您的主线程将完成Main 的执行并退出应用程序。要解决此问题,您需要将 SendReportsToCustomers() 的返回类型更改为以下内容:

    public async Task SendReportsToCustomers()
    

    现在,您实际上可以等待它完成。以阻塞方式:

    reportServices.SendReportsToCustomers().Result();
    

    reportServices.SendReportsToCustomers().Wait();
    

    或使用await进行非阻塞等待:

    await reportServices.SendReportsToCustomers();
    

    【讨论】:

    • 忽略 SendReport (a Task) 的返回值不是好的做法或设计。
    • 您的意思是使用Task.Wait() 不是好习惯?您愿意详细说明吗?
    • 您的 foreach 循环正在调用 SendReport 并忽略返回值。您只需从中删除 await
    • 为什么这很重要?使用SelectTask.WhenAll实际上是如何处理返回值的?
    • 因为我正在使用await... 链接async 方法背后的想法是链接await 调用。或者,至少,用返回的Task某事。你只是完全无视它。充其量这是糟糕的设计。最坏的情况是它是一个错误。这正是您应该避免使用声明为async void 的方法的原因。
    【解决方案3】:

    首先,您应该AVOID async void 方法,ReportsServices.SendReportsToCustomers() 应该返回Task。您的代码调用该方法但不等待它完成,该方法是异步的,因此它在第一次等待时返回。您应该在 Main() 中等待它完成。有两种方法:

    如果您使用的是 C# 7,则允许使用 async Main:

    private static async Task Main(string[] args)
    {
      ...
      var reportServices = new ReportsServices();
    
      await reportServices.SendReportsToCustomers();
    
      Log.CloseAndFlush();  // It executes the first await call then returns here.
    }
    

    如果没有,您将不得不同步等待操作完成:

    private static void  Main(string[] args)
    {
      ...
      var reportServices = new ReportsServices();
    
      reportServices.SendReportsToCustomers().Wait();;
    
      Log.CloseAndFlush();  // It executes the first await call then returns here.
    }
    

    这里有一个article 有进一步的解释。

    【讨论】:

    • 我实现了这个方法并且它有效。谢谢 问题:为什么现在要等待 SendReportToCustomers 完成后再继续?我认为它会继续并关闭应用程序。
    【解决方案4】:

    我所希望的是循环将继续通过所有 客户和发送电子邮件;不必等待电子邮件发送 在继续下一个之前。

    您的循环没有这样做。它实际上是一次处理一封电子邮件。但是,调用线程会立即获得控制权。

    下面的这一行实际上是“在另一个线程上执行此操作,然后在完成后继续运行此方法”。同时,由于await没有阻塞,调用线程继续执行。

    await SendReport(customer.Id, repTypeId, freqName);
    

    我建议这样做,它会启动您的所有报告并执行await 以完成所有报告:

    await Task.WhenAll(_dbContext.Customer.Select(x => SendReport(x.Id, repTypeId, freqName));
    

    我还强烈建议在使用async 时始终返回Task

    public async Task SendReportsToCustomers()
    

    这样,调用者可以await 或对返回值做任何事情。你甚至可以用Task.Result 屏蔽它。

    我不明白为什么我的循环不能继续循环遍历所有 顾客们? - 还有更多工作要做。不知道为什么会跳过 返回主函数。

    请参阅 await documentation 以更好地了解它的用法和非阻塞性质。

    在非常高的层次上,您可以将await 认为是“在此任务完成之前不要执行此方法的其余部分,但也不要阻塞”。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-07-20
      • 1970-01-01
      • 1970-01-01
      • 2021-12-20
      • 2011-08-27
      • 1970-01-01
      • 1970-01-01
      • 2018-03-12
      相关资源
      最近更新 更多