C#异步ApiController路由问题:如何使用Task.WhenAny和Task.Delay解决。

5
在我的服务器上,有一个路由用于为用户生成PDF文件。当生成时间超过指定的时间限制时,该路由应该返回接受状态码,并在处理完成后向用户发送电子邮件。我遇到的问题是无论我将设置多低,Task.Delay仍然没有被执行。有趣的是,当生成抛出异常或完成并且时间超过了限制时,执行过程会继续返回Accepted状态。我怀疑罪魁祸首是死锁情况。我阅读了一个相关的问题,但是还没有能够解决我的问题:C#/.NET 4.5 - Why does "await Task.WhenAny" never return when provided with a Task.Delay in a WPF application's UI thread?有什么明显的问题我忽略了吗?或者我应该知道线程上下文的一些东西吗?
 [HttpPost]
 [Route("foo")]
 public async Task<IHttpActionResult> Foo([FromBody] FooBody fooBody)
 {
        var routeUser = await ValidateUser();

        async Task<Stream> CeatePdfFile()
        {
            var pdf = await createPdfFromFooData(fooBody);

            return await pdfFileToStream(pdf);
        }

        var delay = Task.Delay(PdfGenerationTimeLimitUntilEmail);
        var createPdfTask = CeatePdfFile();
        var firstTaskResolved = await Task.WhenAny(createPdfTask , delay);

        if (firstTaskResolved == createPdfTask)
        {
            var pdfFileStream = await createPdfTask ;
            return new FileActionResult(pdfFileStream);
        }

        // Creating the PDF can take a long time, so just send an email when it's done
        async void SendEmail(CancellationToken token)
        {
            var pdfFileStreamToEmail = await createPdfTask ;
            _emailSender.SendDownloadEmail(routeUser.Email, pdfFileStreanToEmail);
        }

        HostingEnvironment.QueueBackgroundWorkItem(token => SendEmail(token));
        return StatusCode(HttpStatusCode.Accepted);
}

2
“async void SendEmail” 应该改为 “async Task SendEmail” 吗?这样该方法可以使用 await。 - Igor
1
作为一种附带说明(也是个人观点),我更喜欢使用命名约定 ...Async,其中您将 Async 后缀添加到返回 Task(或 Task<T>)的任何方法中。它有助于提高代码的可读性。例如:CreatePdfFileAsync() 和可能的 SendMailAsync - Igor
1
我怀疑 createPdfFromFooData 在产生结果之前会进行长时间的阻塞操作。尝试使用以下代码:var createPdfTask = Task.Run(() => CeatePdfFile()); - Kevin Gosse
@KevinGosse,针对这个问题有一个更简单和更可重用的解决方案 - 请查看答案。 - Marc Gravell
1
关于@Igor的问题:这不是真正的问题 - 在许多情况下,包括MVC中,您绝对不能使用async void - MVC的同步上下文积极阻止 async void - 如果您尝试使用它,它会抛出异常。 - Marc Gravell
显示剩余3条评论
1个回答

2
如果Kevin Gosse的说法是正确的,即真正的问题在于createPdfFromFooDataCeatePdfFile在第一个await之前做了大量的工作:您可以添加Task.Yield()来人为地强制进行额外的等待,从而将其余的工作推到相关上下文的工作队列(或者线程池中):

如果Kevin Gosse的说法是正确的,即真正的问题在于createPdfFromFooDataCeatePdfFile在第一个await之前做了大量的工作:您可以添加Task.Yield()来人为地强制进行额外的等待,从而将其余的工作推到相关上下文的工作队列(或者线程池中):

原始答案翻译成“最初的回答”。
    async Task<Stream> CeatePdfFile()
    {
        await Task.Yield(); // force asynchronicity
        var pdf = await createPdfFromFooData(fooBody);

        return await pdfFileToStream(pdf);
    }

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