使用SendGrid C#客户端库在控制台应用程序中发送电子邮件时,为什么需要使用Wait?

4

我正在使用 SendGrid 的 C# 客户端库,版本号为 9.27.0。

我唯一能发送电子邮件的方法是在我的代码末尾添加 Wait()

我知道 await 运算符是一个承诺,在异步方法完成后返回到代码中的某个点。

但是,为什么需要添加 Wait() 呢?那不是将异步方法转换为同步方法吗?如果真是这样,那么将其设置为异步有什么意义呢?

Program.cs

static void Main(string[] args) {
    //var customerImport = new CustomerImport();
    //customerImport.DoImport();

    var mailClient = new MailClient();
    var recipients = new List<string>();
    recipients.Add("test@lobbycentral.com");

    //Never sends an email
    var response = mailClient.SendMail("noreply@lobbycentral.com", recipients, "Test email", "This is a test of the new SG client", false);

    //Will send an email
    mailClient.SendMail("noreply@lobbycentral.com", recipients, "Test email", "This is a test of the new SG client", false).Wait();
}

MailClient.cs

public async Task SendMail(string emailFrom, List<string> emailTo, string subject, string body, bool isPlainText) {

    try {
        var apiKey = Utils.GetConfigValue("sendgridAPIKey");
        var emails = new List<EmailAddress>();

        foreach (string email in emailTo) {
            emails.Add(new EmailAddress(email));
        }

        var plainTextContent = "";
        var htmlContent = "";

        if (!isPlainText) {
            htmlContent = body;
        } else {
            plainTextContent = body;
        }

        var message = MailHelper.CreateSingleEmailToMultipleRecipients(new EmailAddress(emailFrom, "LobbyCentral"), emails, subject, plainTextContent, htmlContent);

        //if (metaData != null)
        //    message.AddCustomArgs(metaData);

        foreach (string filename in FileAttachments) {
            if (System.IO.File.Exists(filename)) {
                using (var filestream = System.IO.File.OpenRead(filename)) {
                    await message.AddAttachmentAsync(filename, filestream);
                }
            }
        }

        foreach (PlainTextAttachmentM plainTextM in PlainTextAttachments) {
            byte[] byteData = Encoding.ASCII.GetBytes(plainTextM.Content);

            var attachment = new Attachment();
            attachment.Content = Convert.ToBase64String(byteData);
            attachment.Filename = plainTextM.AttachmentFilename;
            attachment.Type = "txt/plain";
            attachment.Disposition = "attachment";

            message.AddAttachment(attachment);
        }
        
        var client = new SendGridClient(apiKey);
        var response = await client.SendEmailAsync(message);

        if (response.IsSuccessStatusCode) {

            if (DeleteAttachmentsAfterSend && FileAttachments.Count > 0) {
                foreach (string filename in FileAttachments) {
                    if (System.IO.File.Exists(filename)) {
                        System.IO.File.Delete(filename);
                    }
                }
            }
        } else {
            Utils.DebugPrint("error sending email");
        }


    } catch (Exception ex) {
        throw new Exception(string.Format("{0}.{1}: {2} {3}", System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName, System.Reflection.MethodBase.GetCurrentMethod().Name, ex.Message, ex.StackTrace));
    }
}

这很有道理,也很清晰。在我们的Web应用程序中,代码可以在没有等待的情况下运行,但我认为这是因为它始终在应用程序池中运行。感谢确认。 - JamesF
4个回答

1

问题 : 但是为什么我需要添加Wait()呢?这不是把异步方法转换成同步方法吗?那这样做的意义在哪里呢?

答案 :Task上添加Wait()并非强制性操作。那么,异步有什么用处呢?它给了你两个很好的好处。

  1. 你可以返回Task,这意味着它是可等待的,这也意味着你可以选择等待它或者忘记它,让任务被线程池线程调度和执行,而不是在同一个线程中同步运行它。
  2. 你可以使用async/await模式,这意味着你可以优雅地避免阻塞当前线程。

下面这段代码显然会阻塞当前线程。

static void Main(string[] args) 
{
    .
    .
    //Will send an email
    mailClient.SendMail("noreply@lobbycentral.com", recipients, "Test email", "This is a test of the new SG client", false)
   .Wait(); // <- block the current thread until the task completes.
}

如果在您真实的代码库中可以使用“SendMail”作为fire&forget,那么您可以直接删除.Wait()并继续进行,而无需检查Task状态。但是您的应用程序应该运行足够长的时间来完成计划任务。如果没有,则最好考虑使用async/await模式,而不是通过使用.Wait()阻塞您宝贵的线程。
static async Task Main(string[] args) 
{
    .
    .
    //Will send an email and get to know when the task is done.
    await mailClient.SendMail("noreply@lobbycentral.com", recipients, "Test email", "This is a test of the new SG client", false);
}

1

调用 mailClient.SendMail启动一个Task,但不会等待其完成。

如果这是你程序的最后一条指令,那么程序将在任务完成之前结束。

你希望最后一条指令开始并等待任务完成。你可以使用以下任一方法实现。

使你的 Main 方法异步。(这是我个人会选择的方法。)

// Signature changed to async Task instead of void.
static async Task Main(string[] args) {
        // (...)
        
        // Added await. Removed Wait.
        await mailClient.SendMail("noreply@lobbycentral.com", recipients, "Test email", "This is a test of the new SG client", false);
}

您可以像之前一样使用Wait

static void Main(string[] args) {
        // (...)
        
        var task = mailClient.SendMail("noreply@lobbycentral.com", recipients, "Test email", "This is a test of the new SG client", false);
        task.Wait();
}

0

你需要在这个控制台应用程序中等待它,因为Main()在返回之前(和应用程序退出之前)执行的最后一件事情是发送邮件。我想几乎没有机会邮件发送任务会在应用程序退出之前完成;它涉及大量的IO,而应用程序退出只需要纳秒级别的时间,将不完整的任务带走。

任何让它等待足够长时间的操作都可以;使用Wait调用将其变成同步,将Main设置为async Task Main(...),甚至添加一个Console.ReadLine都意味着邮件发送将有时间完成..

..尽管我质疑为什么一个只做一件事然后退出的控制台应用程序甚至需要是异步的 - 它在等待IO完成时没有其他工作要做,因此使用任何异步设施似乎都没有多大意义。


0

简而言之 更新你的代码,使用asyncawaitTask在整个方法的调用链中,包括你的Main方法,否则你的程序将不会等待异步Task完成,并且不会运行你的所有代码。

你的代码应该像这样:

static async Task Main(string[] args) {
    //var customerImport = new CustomerImport();
    //customerImport.DoImport();

    var mailClient = new MailClient();
    var recipients = new List<string>();
    recipients.Add("test@lobbycentral.com");

    //Never sends an email
    var response = await mailClient.SendMail("noreply@lobbycentral.com", recipients, "Test email", "This is a test of the new SG client", false);
}

在C# .NET中,Task类表示通常在另一个线程上异步执行的操作。当您使用await关键字或.Wait()方法时,程序将等待操作完成,然后继续执行下一条语句。
使用await关键字和.Wait()方法之间的区别在于,await关键字会释放当前线程,以便可以在当前线程上执行其他工作,而.Wait()方法将阻塞当前线程,使线程未被利用,直到任务完成为止。 当使用async关键字时,任务完成后,程序将查找可用线程(不一定是您启动的同一线程),并继续运行await之后的指令。.Wait()会阻塞当前线程,因此在任务完成后,它将继续使用当前线程。
要使用await,您需要将方法标记为async。有时,您不需要等待Task,而可以直接返回它,在这种情况下,您不需要将方法标记为async,但仍建议这样做以便更轻松地进行调试。 然而,一旦您开始使用asyncTask,则需要一直使用到调用链的最上层。
此示例使用.Wait需要转换为使用async/await
public static class Program
{
    public static void Main(string[] args)
    {
        DoSomeWork();
    }

    public static void DoSomeWork()
    {
        DoFileOperations();
    }

    public static void DoFileOperations()
    {
        File.ReadAllTextAsync("/path/to/file").Wait();
    }
}

将这个程序变成异步的看起来像这样:

public static class Program
{
    public static async Task Main(string[] args)
    {
        await DoSomeWork();
    }

    public static Task DoSomeWork()
    {
        // you can return a Task without awaiting it, as a result no need for async keyword.
        return DoFileOperations();
        
        // But the following is recommended for easier debugging, at cost of a negligible performance hit. (Also need to mark method is async for this)
        // return await DoFileOperations(); 
    }

    public static async Task DoFileOperations()
    {
        await File.ReadAllTextAsync("/path/to/file");
    }
}

对于控制台程序,您需要更新Main方法签名以使用async TaskTask才能正常工作。如果不这样做,Task将开始运行,但程序将立即退出而不等待Task完成。
(非控制台应用程序将提供其他API来执行async/await。)
在您的应用程序中,您的Main方法没有async TaskTask返回签名,并且您没有等待从mailClient.SendMail方法返回的Task
mailClient.SendMail方法内部可能正在发生的情况是:
  1. 第一个异步 Task 是由 message.AddAttachmentAsync 创建并启动的。
  2. 这个 Task 在一个单独的线程上运行,同时当前线程返回到 Main 方法中 mailClient.SendMail 代码之后继续运行。
  3. Main 方法完成后,控制台应用程序停止运行。

此时,来自 message.AddAttachmentAsyncTask 可能已经完成,但是随后的代码从未运行,包括 client.SendEmailAsync 方法,因为没有等待来自 mailClient.SendMailTask

asyncawaitTask有很多微妙之处,但这些细节对于你的问题并不太相关,如果你感兴趣,可以在Microsoft文档中了解更多关于异步编程的信息。
我还建议阅读David Fowler的指导,其中列出了一些陷阱和最佳实践。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接