3

我有一个返回的方法,IEnumerator它对每条记录都有很长的计算过程。我怎样才能让它不完全停留在它的yield return命令上,而是在后台进行下一条记录计算以获得更快的下一次响应?我不太关心线程安全,因为此方法的使用者在不同的类中,而且这两个类彼此非常隔离。

private int[] numbers = new int[] { 45, 43, 76, 23, 54, 22 };

private static int GetFibonacci(int n)
{
    if (n == 0 || n == 1) return n;
    else return GetFibonacci(n - 1) + GetFibonacci(n - 2);
}

public IEnumerator<int> GetFibonaccies()
{
    foreach (int n in numbers)
    {
        int f = GetFibonacci(n); // long job
        yield return f; // << please do not be lazy and do not stuck
                        // here till next request, but calculate next
                        // number in background to quickly respond
                        // your next request
    }
}
4

2 回答 2

2

WithPreloadNext这是s的扩展方法,它将下一次调用IEnumerable<T>卸载到 ,而前一个值被产生给调用者:MoveNextThreadPool

public static IEnumerable<T> WithPreloadNext<T>(this IEnumerable<T> source)
{
    // Argument validation omitted
    using var enumerator = source.GetEnumerator();
    Task<(bool, T)> task = Task.Run(() => enumerator.MoveNext() ?
        (true, enumerator.Current) : (false, default));
    while (true)
    {
        var (moved, value) = task.GetAwaiter().GetResult();
        if (!moved) break;
        task = Task.Run(() => enumerator.MoveNext() ?
            (true, enumerator.Current) : (false, default));
        yield return value;
    }
}

使用示例:

private IEnumerable<int> GetFibonacciesInternal()
{
    foreach (int n in numbers) yield return GetFibonacci(n);
}

public IEnumerable<int> GetFibonaccies() => GetFibonacciesInternal().WithPreloadNext();

注意:卸载MoveNext意味着源可枚举不在调用者的上下文中枚举。因此,如果源可枚举与当前线程具有线程亲和力,则不应使用此方法。例如,在可枚举与 UI 组件交互的 Windows 窗体应用程序中。

于 2021-06-20T18:00:27.497 回答
1

我使用下面的代码对此案例进行了测试。的想法FibonacciTestAsync是并行运行多个计算。

答案分为 C# 7 和 C# 8 部分。

使用 C# 7 时的回答

序列为 40、41、42、43、44、45 的测试持续时间

  • FibonacciTestAsync:~31 秒
  • FibonacciTestNonAsync:~76 秒

请注意,这些结果比下面的 asp.net 核心部分的结果要慢得多。

环境:Windows 10、VS 2017、.NET Framework 4.6.1、NUnit、Resharper

private static readonly int[] Numbers = { 40, 41, 42, 43, 44, 45 };

private static int GetFibonacci(int n)
{
    if (n == 0 || n == 1) return n;
    return GetFibonacci(n - 1) + GetFibonacci(n - 2);
}

private static Task<int> GetFibonacciAsync(int n) => Task.Run(() => GetFibonacci(n));

public static IEnumerator<int> GetFibonaccies()
{
    foreach (var n in Numbers)
    {
        var f = GetFibonacci(n); // long job
        yield return f; // << please do not be lazy and do not stuck here till next request but calculate next number in background to quickly respond your next request
    }
}

// in C# 8: public static async IAsyncEnumerable<int> GetFibonacciesAsync()
public static IEnumerable<int> GetFibonacciesAsync()
{
    var taskList = Numbers
        .Select(GetFibonacciAsync)
        .ToList();

    foreach (var task in taskList)
    {
        // in C# 8: yield return await task;
        yield return task.GetAwaiter().GetResult();
    }
}

private static readonly IList<int> ExpectedOutput = new List<int>
{
    102334155,
    165580141,
    267914296,
    433494437,
    701408733,
    1134903170
};

[Test]
public void FibonacciTestNonAsync()
{
    var sw = new Stopwatch();
    sw.Start();

    var result = new List<int>();

    using (var fibonacciNumberEnumerator = GetFibonaccies())
    {
        while (fibonacciNumberEnumerator.MoveNext())
        {
            result.Add(fibonacciNumberEnumerator.Current);
            Console.WriteLine(fibonacciNumberEnumerator.Current);
        }
    }

    sw.Stop();
    Console.WriteLine("Elapsed={0}", (double)sw.ElapsedMilliseconds / 1000);

    Assert.AreEqual(ExpectedOutput, result);
}

[Test]
public void FibonacciTestAsync()
{
    var sw = new Stopwatch();
    sw.Start();

    var result = new List<int>();

    // here you can play a little bit:
    // Try to replace GetFibonacciesAsync() with GetFibonacciesAsync().Take(1) and observe that the test will run a lot faster
    var fibonacciNumbers = GetFibonacciesAsync();
    foreach (var item in fibonacciNumbers)
    {
        result.Add(item);
        Console.WriteLine(item);
    }
    sw.Stop();
    Console.WriteLine("Elapsed={0}", (double)sw.ElapsedMilliseconds / 1000);

    Assert.AreEqual(ExpectedOutput, result);
}


使用 C# 8 或更高版本时回答

序列为 40、41、42、43、44、45 的测试持续时间

  • FibonacciTestAsync:~11 秒
  • FibonacciTestNonAsync:~26 秒

测试时使用的环境:Windows 10、VS 2019、asp.net Core 5、NUnit、Resharper

// original array from OP, takes too long too compute private static readonly int[] Numbers = { 45, 43, 76, 23, 54, 22 };

private static readonly int[] Numbers = { 40, 41, 42, 43, 44, 45 };

private static int GetFibonacci(int n)
{
    if (n == 0 || n == 1) return n;
    return GetFibonacci(n - 1) + GetFibonacci(n - 2);
}

private static Task<int> GetFibonacciAsync(int n) => Task.Run(() => GetFibonacci(n));

public static IEnumerator<int> GetFibonaccies()
{
    foreach (int n in Numbers)
    {
        var f = GetFibonacci(n); // long job
        yield return f; // << please do not be lazy and do not stuck here till next request but calculate next number in background to quickly respond your next request
    }
}

public static async IAsyncEnumerable<int> GetFibonacciesAsync()
{
    var taskList = Numbers
        .Select(GetFibonacciAsync) // starting task here
        .ToList();

    foreach (var task in taskList)
    {
        yield return await task; // as soon as current task is completed, yield the result
    }
}

private static readonly IList<int> ExpectedOutput = new List<int>
{
    102334155,
    165580141,
    267914296,
    433494437,
    701408733,
    1134903170
};

[Test]
public void FibonacciTestNonAsync()
{
    var sw = new Stopwatch();
    sw.Start();

    var result = new List<int>();

    using IEnumerator<int> fibonacciNumberEnumerator = GetFibonaccies();
    while (fibonacciNumberEnumerator.MoveNext())
    {
        result.Add(fibonacciNumberEnumerator.Current);
        Console.WriteLine(fibonacciNumberEnumerator.Current);
    }

    sw.Stop();
    Console.WriteLine("Elapsed={0}", (double)sw.ElapsedMilliseconds / 1000);

    Assert.AreEqual(ExpectedOutput, result);
}

[Test]
public async Task FibonacciTestAsync()
{
    var sw = new Stopwatch();
    sw.Start();

    var result = new List<int>();

    var fibonacciNumbers = GetFibonacciesAsync();
    await foreach (var item in fibonacciNumbers)
    {
        result.Add(item);
        Console.WriteLine(item);
    }
    sw.Stop();
    Console.WriteLine("Elapsed={0}", (double)sw.ElapsedMilliseconds / 1000);

    Assert.AreEqual(ExpectedOutput, result);
}

于 2021-06-20T13:25:41.393 回答