Reputation: 1728
Details:
I have a game with two independent AIs playing agains each other. Each AI has its own task. Both tasks need to start at the same time, need to take some parameters and return a value. Now I want to run 100-200 games (with each two tasks) in parallel.
The problem that I now have is that the two tasks are not started together. They are started completely random, whenever there are some free resources.
Code:
My current approach is like following.
Because with just that the two AI-Tasks for each game are not started together, I added an AutoResetEvent. I hoped that I could wait with one task until the second task has started but instead the AutoResetEvent.WaitOne blocks all resources. So the result with AutoResetEvent is that the first AI-Tasks are starting and waiting for the second task to start, but since they do not free the threads again they wait forever.
private ConcurrentBag<Individual> TrainKis(List<Individual> population) {
ConcurrentBag<Individual> resultCollection = new ConcurrentBag<Individual>();
ConcurrentBag<Individual> referenceCollection = new ConcurrentBag<Individual>();
Parallel.ForEach(population, individual =>
{
GameManager gm = new GameManager();
CancellationTokenSource c = new CancellationTokenSource();
CancellationToken token = c.Token;
AutoResetEvent waitHandle = new AutoResetEvent(false);
KI_base eaKI = new KI_Stupid(gm, individual.number, "KI-" + individual.number, Color.FromArgb(255, 255, 255));
KI_base referenceKI = new KI_Stupid(gm, 999, "REF-" + individual.number, Color.FromArgb(0, 0, 0));
Individual referenceIndividual = CreateIndividual(individual.number, 400, 2000);
var t1 = referenceKI.Start(token, waitHandle, referenceIndividual).ContinueWith(taskInfo => {
c.Cancel();
return taskInfo.Result;
}).Result;
var t2 = eaKI.Start(token, waitHandle, individual).ContinueWith(taskInfo => {
c.Cancel();
return taskInfo.Result;
}).Result;
referenceCollection.Add(t1);
resultCollection.Add(t2);
});
return resultCollection;
}
This is the start method of the AI where I wait for the second AI to play:
public Task<Individual> Start(CancellationToken _ct, AutoResetEvent _are, Individual _i) {
i = _i;
gm.game.kis.Add(this);
if (gm.game.kis.Count > 1) {
_are.Set();
return Task.Run(() => Play(_ct));
}
else {
_are.WaitOne();
return Task.Run(() => Play(_ct));
}
}
And the simplified play method
public override Individual Play(CancellationToken ct) {
Console.WriteLine($"{player.username} started.");
while (Constants.TOWN_NUMBER*0.8 > player.towns.Count || player.towns.Count == 0) {
try {
Thread.Sleep((int)(Constants.TOWN_GROTH_SECONDS * 1000 + 10));
}
catch (Exception _ex) {
Console.WriteLine($"{player.username} error: {_ex}");
}
//here are the actions of the AI (I removed them for better overview)
if (ct.IsCancellationRequested) {
return i;
}
}
if (Constants.TOWN_NUMBER * 0.8 <= player.towns.Count) {
winner = true;
return i;
}
return i;
}
Is there a better way of doing this, keeping all things but ensure that the two KI-Tasks in each game are started at the same time?
Upvotes: 4
Views: 550
Reputation: 43996
My suggestion is to change the signature of the Play
method so that it returns a Task<Individual>
instead of Individual
, and replace the calls to Thread.Sleep
with await Task.Delay
. This small change should have a significant positive effect to the responsiveness of the AI players, because no threads will be blocked by them, and the small pool of ThreadPool
threads will be optimally utilized.
public override async Task<Individual> Play(CancellationToken ct)
{
Console.WriteLine($"{player.username} started.");
while (Constants.TOWN_NUMBER * 0.8 > player.towns.Count || player.towns.Count == 0)
{
//...
await Task.Delay((int)(Constants.TOWN_GROTH_SECONDS * 1000 + 10));
//...
}
}
You could also consider changing the name of the method from Play
to PlayAsync
, to comply with the guidelines.
Then you should scrape the Parallel.ForEach
method, since it is not async-friendly, and instead project each individual
to a Task
, bundle all tasks in an array, and wait them all to complete with the Task.WaitAll
method (or with the await Task.WhenAll
if you want to go async-all-the-way).
Task[] tasks = population.Select(async individual =>
{
GameManager gm = new GameManager();
CancellationTokenSource c = new CancellationTokenSource();
//...
var task1 = referenceKI.Start(token, waitHandle, referenceIndividual);
var task2 = eaKI.Start(token, waitHandle, individual);
await Task.WhenAny(task1, task2);
c.Cancel();
await Task.WhenAll(task1, task2);
var result1 = await task1;
var result2 = await task2;
referenceCollection.Add(result1);
resultCollection.Add(result2);
}).ToArray();
Task.WaitAll(tasks);
This way you'll have maximum concurrency, which may not be ideal, because the CPU or the RAM or the CPU <=> RAM bandwidth of your machine may become saturated. You can look here for ways of limiting the amount of concurrent asynchronous operations.
Upvotes: 3