Подробности:

У меня есть игра с двумя независимыми ИИ, играющими друг против друга. У каждого ИИ есть своя задача. Обе задачи должны запускаться в одно и то же время, принимать некоторые параметры и возвращать значение. Теперь я хочу запустить 100-200 игр (с каждой из двух задач) параллельно.

Проблема, с которой я столкнулся, заключается в том, что эти две задачи не запускаются вместе. Они запускаются совершенно случайно, когда есть свободные ресурсы.

Код:

Мой текущий подход выглядит следующим образом.

  • У меня есть список объектов ввода, которые включают в себя некоторые параметры. - С помощью Parallel.ForEach я создаю для каждого объекта ввода игру и два ИИ для этой игры. - Тот ИИ, который первым завершает игру, останавливает другой ИИ, играющий в ту же игру, с помощью CancellationToken. - Все возвращаемые значения сохраняются в ConcurrentBag.

Поскольку при этом две AI-задачи для каждой игры не запускаются вместе, я добавил AutoResetEvent. Я надеялся, что смогу подождать с одной задачей, пока не запустится вторая задача, но вместо этого AutoResetEvent.WaitOne блокирует все ресурсы. В результате AutoResetEvent приводит к тому, что первые AI-задачи запускаются и ждут запуска второй задачи, но поскольку они не освобождают потоки снова, они ждут вечно.

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

Это метод запуска ИИ, при котором я жду, пока второй ИИ начнет играть:

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

И упрощенный метод игры

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

Есть ли лучший способ сделать это, сохранив все вещи, но обеспечив одновременный запуск двух KI-заданий в каждой игре?

Ответы (1)

Я предлагаю изменить сигнатуру метода Play так, чтобы он возвращал Task<Individual> вместо Individual, и заменить вызовы Thread.Sleep на await Task.Delay. Это небольшое изменение должно оказать значительное положительное влияние на скорость реакции ИИ игроков, поскольку ни один поток не будет блокироваться ими, а небольшой пул потоков ThreadPool будет использоваться оптимально.

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));
        //...
    }
}

Вы также можете рассмотреть возможность изменения названия метода с Play на PlayAsync, чтобы соответствовать руководящим принципам.

Затем вам следует отказаться от метода Parallel.ForEach, поскольку он не является асинхронным, и вместо этого спроецировать каждого индивидуума на задачу, объединить все задачи в массив и ждать их завершения с помощью метода Task.WaitAll (или с помощью await Task.WhenAll, если вы хотите идти 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);

Таким образом, у вас будет максимальный параллелизм, который может быть не идеальным, потому что процессор или оперативная память или пропускная способность процессора <=> оперативной памяти вашей машины может стать насыщенной. Вы можете посмотреть здесь способы ограничения количества одновременных асинхронных операций.

2022 WebDevInsider