Reputation: 926
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
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:
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:
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
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