在C#中使函数变为非阻塞/异步的简单方法是什么?

4

我有一个按钮,当用户点击它时,会发送一封电子邮件。我希望这个过程能够立即返回并在后台发送电子邮件,而不会因为处理电子邮件而占用UI。我搜索了async/await/等方法,并找到了很多不同的解决方案 - 我正在寻找一个简单的解决方案。我的电子邮件代码:

public void SendEmail(string toAddress, string subject, string body, string code = null) {
    try {

        var fromAddressObj = new MailAddress("noreply@me.com", "Name");
        var toAddressObj = new MailAddress(toAddress, toAddress);
        const string fromPassword = "Password";

        var smtp = new SmtpClient {
            Host = "smtp.office365.com",
            Port = 587,
            EnableSsl = true,
            DeliveryMethod = SmtpDeliveryMethod.Network,
            UseDefaultCredentials = false,
            Credentials = new NetworkCredential(fromAddressObj.Address, fromPassword)
        };
        using (var message = new MailMessage(fromAddressObj, toAddressObj) {
            Subject = subject,
            IsBodyHtml = true

        }) {
            message.Body = body;
            smtp.Send(message);
        }

    } catch (Exception e) {
        Elmah.ErrorSignal.FromCurrentContext().Raise(e);
    }
}

如何修改代码以避免阻塞调用者?

当您拥有操作的“async”版本时,请勿使用“Task.Run”。这会浪费线程并限制可扩展性。请查看我的答案 - i3arnon
@SB2055,理想情况下,您还应该支持取消操作,而SmtpClient.SendMailAsync不支持。您可以参考这里SendMailExImplAsync - noseratio - open to work
2个回答

6

SmtpClient类提供了SendMailAsync方法,使用它来代替Send方法,在发送邮件时可以解除UI线程的阻塞,但你仍然可以处理可能发生的任何异常:

private async void button_Click(object sender, EventArgs e)
{
   await SendEmailAsync(address, subject, body)
}

public async Task SendEmailAsync(string toAddress, string subject, string body, string code = null) 
{
    try 
    {
        var fromAddressObj = new MailAddress("noreply@me.com", "Name");
        var toAddressObj = new MailAddress(toAddress, toAddress);
        const string fromPassword = "Password";

        var smtp = new SmtpClient {
            Host = "smtp.office365.com",
            Port = 587,
            EnableSsl = true,
            DeliveryMethod = SmtpDeliveryMethod.Network,
            UseDefaultCredentials = false,
            Credentials = new NetworkCredential(fromAddressObj.Address, fromPassword)
        };
        using (var message = new MailMessage(fromAddressObj, toAddressObj)
        {
            Subject = subject,
            IsBodyHtml = true
        }) 
        {
            message.Body = body;
            await smtp.SendMailAsync(message);
        }
    }
    catch (Exception e) 
    {
        Elmah.ErrorSignal.FromCurrentContext().Raise(e);
    }
}

如果async方法的同步部分(即await之前的部分)仍然会阻塞UI线程,您可以将其转移到ThreadPool线程:
private async void button_Click(object sender, EventArgs e)
{
   await Task.Run(() => SendEmailAsync(address, subject, body))
}

注:

  • 不要在没有使用await的情况下启动一个Task,除非您处于无法使用await的情况下(UI事件处理程序不是这种情况之一)
  • async void仅适用于UI事件处理程序。不要在其他任何地方使用它。

1
您介意解释一下为什么所选答案是错误的吗?假设没有操作的异步版本,而我仍然需要在不阻塞的情况下运行它 - 那么显然我不能使用await,对吧?因此,在某些情况下,使用Task而不使用await是可取的?对于我的幼稚,表示歉意,任何解释都将有所帮助。 - SB2055
1
如果没有异步版本(async或其他版本),那么 Task.Run 就非常适合。但是你仍然需要等待一个 Task,这个 TaskTask.Run 返回的。在你的情况下,没有不等待 Task 的好理由。 - i3arnon
我认为告诉提问者什么对他来说是必要的是相当荒谬的。决定他认为什么是必要的。如果他不想等待函数,告诉他他必须这样做并不是回答他的问题。你的技术回答非常好...但它并不是这个问题的答案。 - nvoigt
1
@nvoigt 不,对于OP来说必要的是操作应该是“非阻塞”的,并且“立即返回并在后台发送电子邮件而不会因为处理电子邮件而阻塞UI”。这可以通过简单地使用async-await来实现。没有理由不去await返回的Task,这样做是不好的。如果出于某种原因OP真的不能使用await,那么至少要添加一个继续处理任何问题的处理程序,使用ContinueWith - i3arnon
1
@nvoigt 这不是民主,你不应该因为被踩而删除你的回答,就像我没有因为你的回答被赞和采纳或者你踩了我的回答而删除它一样。如果你认为你的回答是正确的,那么你应该保留它。当你意识到它是错误的时候,你应该将其删除。 - i3arnon
显示剩余2条评论

5
您可以将方法放在任务中运行,而无需等待它完成:
private void button_Click(object sender, EventArgs e)
{
   Task.Run( () => SendEMail(address, subject, body) );
}

4
不需要使用 Task.RunSmtpClient 本身就有自然的异步 API。 - Yuval Itzchakov
1
Task.Run 适用于 CPU 密集型操作。这是一个 I/O 密集型操作。正如 @YuvalItzchakov 所提到的,SmtpClient 已经有了 SendMailAsync 操作。虽然这个方案可行,但并不是最佳解决方案。 - Cameron
你可以自己提供一个答案并建议一种不会阻塞用户界面且能够立即继续的版本,这样怎么样?这里的另一个答案并不能满足原帖作者的“调用者不被阻塞”的要求。它通过等待来阻塞进一步的语句执行。这就是等待某些东西的意义所在。 - nvoigt
1
@nvoigt,另一个答案没有阻塞(这就是等待某些东西的重点)。请参见此评论 - i3arnon

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