使用任务重新尝试异步函数——哪种方法更有效?

本文关键字:方法 有效 函数 任务 新尝试 异步 | 更新日期: 2023-09-27 17:54:53

我想知道哪种方法在内存和资源使用方面更有效。

特别是方法#1,我很难想象任务对象将如何创建和线程旋转?有谁能详细解释一下被子下面发生了什么吗?

如果两者之间没有区别,我想使用#1(想避免冒泡异步)。对于#2,我理解编译器将在下面生成一个状态机并产生返回。OTOH, #1在概念上似乎是递归的,但它在传统意义上是递归的吗?一个堆栈框架等待另一个堆栈框架?

方法# 1:

internal static Task ExecuteAsyncWithRetry(Func<Task> methodToExecute, Func<bool> shouldRetry)
    {
        var tcs = new TaskCompletionSource<object>();
        try
        {
            return methodToExecute().ContinueWith<Task>((t) =>
            {
                if (t.IsFaulted || t.IsCanceled)
                {
                    if (shouldRetry())
                    {
                        return ExecuteAsyncWithRetry(methodToExecute, shouldRetry);
                    }
                    else
                    {
                        tcs.SetException(t.Exception);
                    }
                }
                else
                {
                    tcs.SetResult(null);
                }
                return tcs.Task;
            }, TaskContinuationOptions.ExecuteSynchronously).Unwrap();
        }
        catch(Exception ex)
        {
            tcs.SetException(ex);
        }
        return tcs.Task;
    }

方法#2(忽略两者之间异常传播的差异):

internal static async Task ExecuteWithRetry(Func<Task> methodToExecute, Func<bool> shouldRetry)
    {
        while (true)
        {
            try
            {
                await methodToExecute();
            }
            catch(Exception ex)
            {
                if(!shouldRetry())
                {
                    throw;
                }
            }
        }
    }

使用任务重新尝试异步函数——哪种方法更有效?

除了不同的异常和取消传播之外,还有另一个主要区别。

在第一种情况下,由于TaskContinuationOptions.ExecuteSynchronously的原因,您的continuation运行在任务已经完成的同一个线程上。

在第二种情况下,它将在原始同步上下文中运行(如果在具有同步上下文的线程上调用methodToExecute)。

尽管第一种方法可能更有效,但它也可能很难理解(特别是当你或其他人在一年后再次使用它时)。

我会遵循KISS原则,坚持第二个原则,只有一个修改:

await methodToExecute().ConfigureAwait(false);

更新以解决注释:

"OTOH, #1在概念上似乎是递归的,但它会递归吗传统意义上是一个堆栈框架等待另一个堆栈框架?"

对于#1,它是在同一堆栈帧上递归地发生,还是在不同的堆栈帧上异步发生,完全取决于methodToExecute 内部发生了什么。在大多数情况下,如果在methodToExecute中使用一些自然的异步api,就不会有传统的递归。例如,HttpClient.GetStringAsync在一个随机的IOCP池线程上完成,Task.Delay在一个随机的worker池线程上完成。

然而,即使是异步API也可能在同一个线程上同步完成(例如MemoryStream.ReadAsyncTask.Delay(0)),在这种情况下会有递归。

或者,在methodToExecute中使用TaskCompletionSource.SetResult也可能触发同步延续。

如果你真的想避免任何递归的可能性,通过Task.Run(或Task.Factory.StartNew/Task.Unwrap)调用methodToExecute。或者,更好的是,删除TaskContinuationOptions.ExecuteSynchronously

对于#2,即使在初始线程上存在同步上下文,也可能出现相同的情况。