From a game loop i want to start work in the background that should be executed one after another but should not block the game loop.
So ideally a class Background
How about this
void Main()
{
var executor = new MyExecutor();
executor.Execute(()=>Console.WriteLine("Hello"));
executor.Execute(()=>Console.WriteLine(","));
executor.Execute(()=>Console.WriteLine("World"));
}
public class MyExecutor
{
private Task _current = Task.FromResult(0);
public void Execute(Action action)
{
_current=_current.ContinueWith(prev=>action());
}
}
UPD
Updated code. Now we able to get number of actions, push from different threads, etc.
void Main()
{
var executor = new MyExecutor();
executor.Execute(() => Console.WriteLine("Hello"));
executor.Execute(() => Thread.Sleep(100));
executor.Execute(() => Console.WriteLine(","));
executor.Execute(() => { throw new Exception(); });
executor.Execute(() => Console.WriteLine("World"));
executor.Execute(() => Thread.Sleep(100));
executor.WaitCurrent();
Console.WriteLine($"{nameof(MyExecutor.Total)}:{executor.Total}");
Console.WriteLine($"{nameof(MyExecutor.Finished)}:{executor.Finished}");
Console.WriteLine($"{nameof(MyExecutor.Failed)}:{executor.Failed}");
}
public class MyExecutor
{
private Task _current = Task.FromResult(0);
private int _failed = 0;
private int _finished = 0;
private int _total = 0;
private object _locker = new object();
public void WaitCurrent()
{
_current.Wait();
}
public int Total
{
get { return _total; }
}
public int Finished
{
get { return _finished; }
}
public int Failed
{
get { return _failed; }
}
public void Execute(Action action)
{
lock (_locker) // not sure that lock it is the best way here
{
_total++;
_current = _current.ContinueWith(prev => SafeExecute(action));
}
}
private void SafeExecute(Action action)
{
try
{
action();
}
catch
{
Interlocked.Increment(ref _failed);
}
finally
{
Interlocked.Increment(ref _finished);
}
}
}
One solution is from the excellent Threading in C# E-book. In their section on basic structures, the author makes almost exactly what you're looking for in an example.
Follow that link and scroll down to Producer/consumer queue.
In a later section, he addresses that while ConcurrentQueue would work fine too, it performs worse in all cases EXCEPT in highly-concurrent scenarios. But for your low-load case, it may be better just to get something working easily. I don't have personal experience with the claim from the document, but it's there for you to evaluate.
I hope this helps.
Edit2: upon Evk's suggestion (thanks!), the BlockingCollection class looks like what you want. By default it uses a ConcurrentQueue under the hood. I particularly like the CompleteAdding method, as well as the ability to use CancellationTokens with it. "Shutdown" scenarios are not always correctly accounted for when things are blocking, but this does it right IMO.
Edit 3: As requested, a sample of how this would work with a BlockingCollection. I used the foreach
and GetConsumingEnumerable
to make this even more compact for the consumer side of the problem:
using System.Collections.Concurrent;
private static void testMethod()
{
BlockingCollection<Action> myActionQueue = new BlockingCollection<Action>();
var consumer = Task.Run(() =>
{
foreach(var item in myActionQueue.GetConsumingEnumerable())
{
item(); // Run the task
}// Exits when the BlockingCollection is marked for no more actions
});
// Add some tasks
for(int i = 0; i < 10; ++i)
{
int captured = i; // Imporant to copy this value or else
myActionQueue.Add(() =>
{
Console.WriteLine("Action number " + captured + " executing.");
Thread.Sleep(100); // Busy work
Console.WriteLine("Completed.");
});
Console.WriteLine("Added job number " + i);
Thread.Sleep(50);
}
myActionQueue.CompleteAdding();
Console.WriteLine("Completed adding tasks. Waiting for consumer completion");
consumer.Wait(); // Waits for consumer to finish
Console.WriteLine("All actions completed.");
}
I added in the Sleep() calls so that you can see that things are added while other things are being consumed. You can also choose to launch any number of that consumer
lambda (just call it an Action
, then launch the Action
multiple times) or the addition loop. And at any time you can call Count
on the collection to get the number of tasks that are NOT running. Presumably if that's non-zero, then your producer Tasks are running.