首页 > 解决方案 > 如何并行处理潜在的异步聚合任务并在每个任务在 Unity 中完成时对其进行处理

问题描述

我让自己陷入了这样一种情况,在“正常”的 c# 领域可能相对容易解决,但由于 Unity 的警告有点复杂。

高层次是我有一组需要运行的任务,其中一些可以并行,其中一些是异步的,其他则不是。对于那些可以并行运行的,当每个任务完成时,我需要触发一个事件(在主线程中),让任何感兴趣的人知道任务已经完成。

为了深入了解细节,这是针对 Unity 中的游戏,但是此特定代码要求它不能触及 Unity API,因为它将位于一个纯 c# 的单独项目中。

从概念上讲,在我的游戏中,我有一个“日过渡”。每天晚上当玩家上床睡觉(或因疲惫而昏倒/被敌人击倒)时,游戏将进入加载屏幕以处理我不想在游戏过程中进行的长时间运行的操作。诸如运行模拟以更新日常经济,或文件 I/O 以加载第二天要使用的东西之类的事情。

这些获取可能是线程安全的,也可能不是线程安全的,实际上可能是也可能不是真正的异步调用(例如,文件加载将是)。

我的目标是以尽可能少地阻塞主线程的方式来做到这一点(一些阻塞将会发生,这是不可避免的,因为某些任务可能会触及主线程上需要发生的 Unity API。那些不是线程安全)。

因为有些事情会是异步的,所以我将所有事情都视为可等待的。

我的 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*

只是为了澄清一下,“线程安全”操作旨在在其他线程(真正并行)上运行(或至少能够运行),而不是简单地在主线程上同时运行。当它们完成时,我需要拍摄该事件特别需要在主线程上发出的事件。

标签: c#unity3dparallel-processingasync-await

解决方案


我不确定如何以一种允许它们并行运行的方式启动它们,同时在每个人完成时发送一个事件

解决这个问题的最简单方法是引入一种新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)。


推荐阅读