Как обрабатывать потенциально асинхронные агрегатные задачи параллельно и обрабатывать их по мере выполнения каждой задачи в Unity - PullRequest
0 голосов
/ 28 февраля 2019

Я попал в ситуацию, которую в "нормальной" среде c #, вероятно, было бы относительно легко решить, но она немного сложнее с предостережениями Unity.

Высокий уровень - это IУ меня есть набор задач, которые нужно выполнить, некоторые из которых могут быть параллельными, а некоторые асинхронными, другие нет.Для тех, кто может быть запущен параллельно, когда каждая задача завершается, мне нужно запустить событие (в главном потоке), чтобы сообщить всем заинтересованным сторонам, что задача была выполнена.

Чтобы углубиться в детали,это для игры в Unity, однако этот конкретный код требует, чтобы он не касался Unity API, как это будет в отдельном проекте с чистым c #.

Концептуально в моей игре яесть "День Перехода".Каждую ночь, когда игрок ложится спать (или теряет сознание от истощения / поражается врагами), игра выходит на экран загрузки, чтобы обрабатывать более длительные операции, которые я не хотел бы выполнять в середине игрового процесса.Такие вещи, как запуск симуляции для обновления ежедневной экономики или файловый ввод-вывод для загрузки вещей, которые будут использоваться в течение следующего дня.

Эти дубли могут быть, а могут и не быть потокобезопасными, и на самом деле могут быть или не быть истинными асинхронными вызовами (например, загрузка файлов будет).

Моя цель - сделать этоспособ, который блокирует основной поток как можно меньше (может произойти некоторое блокирование, это неизбежно, поскольку некоторые задачи могут касаться API Unity, который требуется для выполнения в основном потоке. Это не потокобезопасно).

Поскольку некоторые вещи будут асинхронными, я воспринимаю все как ожидаемое.

В моем классе DayTransitioner есть список объектов IDayTransitionAction:

public interface IDayTransitionAction
{
    bool IsRepeating { get; }
    bool IsThreadSafe { get; }
    Task ProcessTransitionAsync();
}

В действительности существуют два разных процесса, которые должны произойтиВот.

Во-первых, код, который является потокобезопасным (асинхронным или нет), должен иметь возможность запускаться таким образом, чтобы они могли фактически работать параллельно, а по завершении события событие должно запускаться наосновной поток.

protected async Task ProcessThreadSafeTasks()
{
    //1st kick off in parallel, probably using one of the Task 
    //methods such as WhenAll or WhenAny or some other voodoo...
    //Not really sure what API calls or structure to use to do it...

    //As each comes back an event needs to be fired. 
    //The event needs to occur on the main thread.
    //this.OnIndividualItemProcessed();
}

Вторым является то, что вещи, которые не являются потокобезопасными, нужно запускать и ожидать (я понимаю, что синхронный процесс в этой группе заблокирует основной поток, но когда одиндействительно асинхронно, это не должно).

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

protected async Task ProcessNonThreadSafeTasks()
{
    foreach (var actionItem in this.nonThreadSafeActions)
    {
        await actionItem.ProcessTransitionAsync();

        //Raise the OnIndividualItemProcessed event so eventually
        //a loading bar can be filled in as tasks complete, though
        //this class doesn't know or care about the loading bar
        //specifically. Just that interested parties want to know
        //when an individual task is completed
        this.OnIndividualItemProcessed();
    }
}

Это тот неприятный первый случай, который вызывает у меня головную боль, так как я не совсем уверен, как запустить их таким образом, который позволяетони должны выполняться параллельно, а также отправлять событие по мере завершения каждого отдельного события (особенно учитывая контекст синхронизации Unity ... Я предполагаю, что мне нужно как-то временно отменить это).Как мне выполнить это?

Заранее спасибо!

Кроме того, для ссылки на метод, который вызовет оба из вышеперечисленных ...

public async Task ProcessTransitionActions()
{
    //Used primarily to lock down the lists while processing is ongoing.
    //The Add method will dump anything that's attempting to be stored
    //to another list to be sorted and added to the main lists after processing
    //is finished.
    IsProcessing = true;

    await this.ProcessThreadSafeTasks();
    await this.ProcessNonThreadSafeTasks();

    //Purge any actions that aren't repeating.
    this.CleanNonRepeatingActions();
    //Add any actions that were stored while processing to the main lists.
    this.SortStoredActions();

    IsProcessing = false;

    //Send out an event to allow interested parties to know processing is
    //finished.
    this.OnProcessingCompleted();
}

Редактировать 1 *

Просто для пояснения, «потокобезопасные» операции предназначены для запуска (или, по крайней мере, для выполнения) в других потоках (true-параллельный) в противоположностьпросто одновременно в основной теме.Когда они завершены, и мне нужно отстрелить событие, оно должно быть выделено в основном потоке.

Ответы [ 2 ]

0 голосов
/ 28 февраля 2019

Я не совсем уверен, как запустить их таким образом, чтобы они могли выполняться параллельно, а также отправлять событие по мере того, как каждый из них завершает

Самый простойЧтобы решить эту проблему, нужно ввести новый метод async.Проще внедрить новый метод async, чем пытаться создать коллекцию и обработать их по мере их завершения, добавив к ним новое действие.Вместо этого вы хотите сначала составить элемент действия и событие, а , а затем сделать их одновременно.(Это «параллельный», а не «параллельный», поскольку мы используем только один поток).

Новый метод async, сочетание двух:

private async Task ProcessActionAndRaiseEvent(ActionItem actionItem)
{
  await actionItem.ProcessTransitionAsync();
  this.OnIndividualItemProcessed();  
}

Тогдавы можете запускать их одновременно:

protected async Task ProcessNonThreadSafeTasks()
{
  var tasks = this.nonThreadSafeActions.Select(ProcessActionAndRaiseEvent).ToList();
  await Task.WhenAll(tasks);
}

Select(..).ToList запустит все задачи.Однако синхронные задачи блокируют , что может задержать запуск некоторых действительно асинхронных задач.Возможно, вы захотите добавить обозначение для «действительно асинхронных» задач и отсортировать их сначала в вашей коллекции nonThreadSafeActions.Таким образом, все асинхронные задачи будут запущены, а затем - задачи блокировки.

Если вы хотите запустить (возможно, блокирующую) логику перехода в потоках пула потоков, вы можете выполнить некоторое простое распараллеливание, вызвав их с помощью Task.Run:

private async Task ProcessActionAndRaiseEvent(ActionItem actionItem)
{
  await Task.Run(() => actionItem.ProcessTransitionAsync());
  this.OnIndividualItemProcessed();  
}

Затем они составляются так же, как и в предыдущем примере (с использованием Task.WhenAll).

0 голосов
/ 28 февраля 2019

Возможно, вам нужно что-то вроде этого.

Не особенно эффективно, если у вас их тысячи, но если у вас есть несколько десятков, все должно работать нормально.

class TransitionRunner
{
    readonly List<IDayTransitionAction> list = new List<IDayTransitionAction>();
    readonly HashSet<Task> runningTasks = new HashSet<Task>();

    public async Task runAll()
    {
        Debug.Assert( 0 == runningTasks.Count );

        foreach( var dta in list )
        {
            if( dta.IsThreadSafe )
            {
                // Thread safe tasks are started and finished on the thread pool's threads
                Func<Task> fnRun = dta.ProcessTransitionAsync;
                runningTasks.Add( Task.Run( fnRun ) );
            }
            else
            {
                // Non-thread safe tasks are started and finished on the GUI thread.
                // If they're not actually async, the following line will run the complete task.
                Task task  = dta.ProcessTransitionAsync();
                if( task.IsCompleted )
                {
                    // It was a blocking task without await
                    // Or maybe await that completed extremely fast.
                    if( task.IsCompletedSuccessfully )
                        OnIndividualItemProcessed();
                    else
                        await task; // Propagate exception
                }
                else
                    runningTasks.Add( task );
            }
        }

        while( runningTasks.Count > 0 )
        {
            runningTasks.Remove( await Task.WhenAny( runningTasks ) );
            OnIndividualItemProcessed();
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...