从 TaskCompletionSource 获取方法的 return 值

Get return value of method from TaskCompletionSource

我有一个生成 PDF 文件的方法 RenderReport (byte[])。这有时会无限期挂起。成功时应该不会超过 15 秒即可完成。因此,我使用 TaskCompletionSource 来限制执行时间并在超过超时时抛出 TimeoutException

但是,我无法确定的是:如何将RenderReport返回的byte[]文件提供给以下代码中的SetResultlongRunningTask.Wait returns 一个布尔值而不是文件那么你从哪里得到文件?

我不想使用 longRunningTask.Result,因为 introduce deadlock issues。这是我的代码:

public async Task RunLongRunningTaskAsync()
{
    Task<byte[]> longRunningTask = Task.Run(() => RenderReport());
    TaskCompletionSource<byte[]> tcs = new TaskCompletionSource<byte[]>();
    Task toBeAwaited = tcs.Task;

    new Thread(() => ThreadBody(longRunningTask, tcs, 15)).Start();

    await toBeAwaited;
}

private void ThreadBody(Task<byte[]> longRunningTask, TaskCompletionSource<byte[]> tcs, int seconds)
{
    bool completed = longRunningTask.Wait(TimeSpan.FromSeconds(seconds));

    if (completed)
        // The result is a placeholder. How do you get the return value of the RenderReport()?
        tcs.SetResult(new byte[100]);
    else
        tcs.SetException(new TimeoutException("error!"));
}

private byte[] RenderReport()
{
    using (var report = new Microsoft.Reporting.WinForms.LocalReport())
    {
        // Other logic here...
        
        var file = report.Render("PDF", null, out _, out _, out _, out _, out var warnings);

        if (warnings.Any())
        {
            // Log warnings...
        }

        return file; // How do I get this file?
    }
}

对我有用的是使用 ContinueWith:

这显示第一个 运行 在指定的 2 秒内成功检索,然后是第二个 运行 超时。

所以这只是一种方法,但这有什么用吗?

using System;
using System.Threading;
using System.Threading.Tasks;

namespace task_completion
{
    class Program
    {
        static void Main(string[] args)
        {
            runAsync();
            Console.ReadKey();            
        }

        static async void runAsync()
        {
            // Normal
            await longRunningByteGetter(1000, new CancellationTokenSource(2000).Token)                
                .ContinueWith((Task<byte[]> t)=> 
                {
                    switch (t.Status)
                    {
                        case TaskStatus.RanToCompletion:
                            var bytesReturned = t.Result;
                            Console.WriteLine($"Received {bytesReturned.Length} bytes");
                            break;
                        default:
                            Console.WriteLine(nameof(TimeoutException));
                            break;
                    }
                });

            // TimeOut
            await longRunningByteGetter(3000, new CancellationTokenSource(2000).Token)
                .ContinueWith((Task<byte[]> t) =>
                {
                    switch (t.Status)
                    {
                        case TaskStatus.RanToCompletion:
                            var bytesReturned = t.Result;
                            Console.WriteLine($"Received {bytesReturned.Length} bytes");
                            break;
                        default:
                            Console.WriteLine(nameof(TimeoutException));
                            break;
                    }
                });
        }

        async static Task<Byte[]> longRunningByteGetter(int delay, CancellationToken token)
        {
            await Task.Delay(delay, token); // This is a mock of your file retrieval
            return new byte[100];
        }
    }
}

如果您同步等待 Task 完成,您只会冒死锁的风险。

如果您知道 longRunningTask 已经完成,那么访问 longRunningTask.Result 是绝对安全的。所以就这样做:

    if (completed)
        tcs.SetResult(longRunningTask.Result);
    else
        tcs.SetException(new TimeoutException("error!"));

甚至比这更复杂:即使您同步等待 longRunningTask 完成,您的案例也不会死锁,因为 longRunningTask 不依赖于任何等待。很值得理解为什么会出现这种死锁情况。


也就是说,放弃一个卡在某个地方的线程听起来是个糟糕的主意:该线程将永远存在泄漏资源,随着更多报告挂起,您将积累它们,直到您 运行内存不足。

如果您真的搞不清楚发生了什么,我建议围绕报告生成编写一个小型包装应用程序,然后 运行 将其作为一个单独的过程。终止进程是 well-supported,让您确保没有任何泄漏。您可以 return 通过标准输出返回报告的字节数。