meir
meir

Reputation: 926

IEnumerator method to calculate next result in background for quicker response

I have a method that returns IEnumerator and it has a long calculation process on each record. How can I make it not fully stuck on its yield return command but work on next record calculation in background for quicker next response? I do not care much about thread safety much because the consumer of this method is in a different class, and these two classes are pretty isolated to each other.

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
    }
}

Upvotes: 3

Views: 299

Answers (2)

Roar S.
Roar S.

Reputation: 11164

I used the code below to run a test for this case. The idea in FibonacciTestAsync is to run multiple computations in parallel.

Answer is divided into a C# 7 and a C# 8 part.

Answer when using C# 7

Test durations with sequence 40, 41, 42, 43, 44, 45:

  • FibonacciTestAsync: ~31 seconds
  • FibonacciTestNonAsync: ~76 seconds

Please note that these results are significantly slower than results from the asp.net Core-section below.

Environment: 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);
}


Answer when using C# 8 or higher

Test durations with sequence 40, 41, 42, 43, 44, 45:

  • FibonacciTestAsync: ~11 seconds
  • FibonacciTestNonAsync: ~26 seconds

Environment used during testing: 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);
}

Upvotes: 1

Theodor Zoulias
Theodor Zoulias

Reputation: 43812

Here is an extension method WithPreloadNext for IEnumerable<T>s, that offloads the next MoveNext invocation to the ThreadPool while the previous value is yielded to the caller:

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;
    }
}

Usage example:

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

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

Note: Offloading the MoveNext means that the source enumerable is not enumerated on the context of the caller. So this method should not be used in case the source enumerable has thread affinity to the current thread. For example in case of a Windows Forms application where the enumerable interacts with UI components.

Upvotes: 2

Related Questions