如何并行处理潜在的异步聚合任务,并在Unity中完成每个任务的处理

时间:2019-02-27 23:27:17

标签: c# unity3d parallel-processing async-await

我陷入一种困境,在“正常”的c#领域中,可能相对容易解决,但由于Unity的警告而变得更加复杂。

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

要深入研究细节,这是针对Unity中的游戏的,但是此特定代码要求不能接触Unity API,因为它将在纯c#的单独项目中。

从概念上讲,我有一个“ Day Transition”。每天晚上,当玩家上床睡觉(或从精疲力尽/被敌人击倒而昏昏欲睡)时,游戏将进入加载屏幕,以处理更长的运行操作,而这是我在游戏过程中不想做的。诸如运行模拟以更新日常经济性之类的事情,或文件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 *

仅需澄清一下,“线程安全”操作旨在在其他线程(真正并行)上运行(或至少能够运行),而不是在主线程上简单地同时运行。就在他们完成任务的时候,我需要将事件专门发送到主线程上。

2 个答案:

答案 0 :(得分:2)

  

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

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

答案 1 :(得分:0)

您可能需要这样的东西。

如果有成千上万个,效率不是特别高,但是如果有几十个,就可以正常工作。

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