使用异步/等待时,我的控制台应用程序过早关闭?

My console app shutdown prematurely when using async / await?

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

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 函数。

我所希望的是循环将继续遍历所有客户并发送电子邮件;无需等待电子邮件发送后再继续下一封。

感谢您的帮助!

What I was hoping for is that loop would continue through all the customers and send emails; not having to wait for the email to be sent before continuing on the next one.

你的循环没有这样做。它实际上是一次处理一封电子邮件。但是,调用线程正在立即取回控制权。

下面这一行实际上是 "do this on another thread, then continue running this method once that's complete"。与此同时,由于 await 没有阻塞,调用线程继续执行。

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

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

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

我也强烈建议在使用 async:

时总是 return a Task
public async Task SendReportsToCustomers()

这样调用者就可以 await 或者用 return 值做任何它想做的事。你甚至可以用 Task.Result.

来阻止它

What I don't get is why wouldn't my loop continue looping through all the customers? - there is more work for it. Not sure why it would skip back to the Main function.

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

在非常高的层次上,您可以将 await 看作是在说 "don't execute the rest of this method until this Task completes, but don't block either"。

看来您不明白await是如何工作的。每次你在函数上调用 await 时,调用函数本身将暂时 return,直到等待的函数 returns。

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

public async Task SendReportsToCustomers()

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

reportServices.SendReportsToCustomers().Result();

reportServices.SendReportsToCustomers().Wait();

或在 await 的非阻塞等待中:

await reportServices.SendReportsToCustomers();

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

如果您使用的是 C# 7,则允许异步 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 以及进一步的解释。

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

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