【问题标题】:How to process potentially async aggregate tasks in parallel and process them as each task completes in Unity如何并行处理潜在的异步聚合任务并在每个任务在 Unity 中完成时对其进行处理
【发布时间】:2019-03-01 08:44:18
【问题描述】:

我让自己陷入了这样一种境地,在“正常”的 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# unity3d parallel-processing async-await


    【解决方案1】:

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

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

    【讨论】:

    • 我实际上想要并行(多线程),而不仅仅是并发操作,用于线程安全操作。
    • 我想知道,在您建议的组合方法中,我是否改为 await Task.Run(() => actionItem.ProcessTransitionAsync()) 是否会将它们发送到线程池, 当任务在主线程上完成时执行事件。那么问题是是否保证它们在主线程以外的线程上运行......
    • @TylerW:抱歉,我根据你的问题(以及变量名nonThreadSafeActions)解释说这些操作确实需要主线程,而你只是想要真正的异步同时运行的。如果您拥有所有线程安全操作,那么您可以按照您的建议更改一行:await actionItem.ProcessTransitionAsync(); 变为 await Task.Run(() => actionItem.ProcessTransitionAsync());Task.Run 保证转换在线程池线程上运行,OnIndividualItemProcessed 将在 UI 线程上运行。
    • 嘿,是的,我实际上两者都在做。我有一个线程安全操作列表和一个非线程安全操作列表。对于非线程安全的,我不关心并发,因为我知道它是异步和非异步代码的混合。无论如何,它都会阻塞直到它到达一个真正的异步方法。我可能想对它们进行排序,以便每隔一段时间进行一次真正的异步操作,而不是仅仅添加顺序(这样 UI 动画就可以移动),但是一旦我测量了性能,我就会这样做。
    • 但是请注意,当您有时间时,您可以编辑答案以指向 cmets,然后我可以将其标记为已接受的答案(只是想确保最终搜索的人看到 cmets关于 task.run() 的部分并行执行)
    【解决方案2】:

    你可能需要这样的东西。

    如果你有数千个,效率不是特别高,但如果你有几十个应该可以正常工作。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多