theoretisch
theoretisch

Reputation: 1728

Execute multiple pairs of concurrent tasks in parallel

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

Answers (1)

Theodor Zoulias
Theodor Zoulias

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

Related Questions