【问题标题】:How to queue up delegates to be executed in series in the background with C#?如何使用 C# 将委托排队以在后台串行执行?
【发布时间】:2016-12-21 20:07:04
【问题描述】:

我想从一个游戏循环开始在后台工作,应该一个接一个地执行,但不应该阻塞游戏循环。

所以理想情况下可以像这样使用一个类BackgroundQueue

BackgroundQueue myQueue = new BackgroundQueue();
//game loop called 60 times per second
void Update()
{
    if (somethingHappens)
    {
        myQueue.Enqueue(() => MethodThatTakesLong(someArguments))
    }   
}

.NET 中是否有适用于该场景的现成类?或者有人知道如何实现BackgroundQueue 类的好例子吗?

如果该类可以报告它当前是否正在做某事以及有多少代表在排队,那就太好了......

【问题讨论】:

  • 和ConcurrentQueue一起看BlockingCollection,例子很多。启动一个从 BlockingCollection 消费的线程,并从你的主循环中启动项目。
  • Task 如果您希望您的委托按顺序执行,则自定义 TaskScheduler 可能会起到作用。有关如何构建自定义 TaskScheduler 的信息,请参阅 infoworld.com/article/3063560/application-development/…。如果您不介意您的委托被并行执行,那么标准的 .NET TaskScheduler 可能就可以了。
  • 考虑将所有内容都放在一个线程上,并使用 async-await 来构建异步工作流。请记住,异步不是并发。您可以在一个线程上执行所有异步工作流,就像您可以同时煮鸡蛋和吐司而无需雇用两名厨师。
  • 因为它是一个高性能应用程序,CPU 负载将是一个非常关键的因素,因此在另一个线程上执行委托非常重要。但据我了解,如果使用异步/任务模式,标准.NET 调度程序将在当前线程处于限制状态时执行此操作。所以也许用 task.ContinueWith 建立一个队列可能是一种选择。我在这里找到了一个例子:stackoverflow.com/a/15120092/355485

标签: c# .net multithreading task backgroundworker


【解决方案1】:

一个解决方案来自优秀的Threading in C# E-book。在他们关于基本结构的部分中,作者几乎完全符合您的要求in an example

点击该链接并向下滚动到生产者/消费者队列。

在后面的部分中,他指出虽然ConcurrentQueue 也可以正常工作,但它在所有情况下的表现都较差,除了在高度并发的场景中。但是对于您的低负载情况,最好让某些东西轻松工作。我对文档中的声明没有个人经验,但您可以对其进行评估。

我希望这会有所帮助。

Edit2:根据 Evk 的建议(谢谢!),BlockingCollection 类看起来像你想要的。默认情况下,它在后台使用 ConcurrentQueue。我特别喜欢 CompleteAdding 方法,以及使用 CancellationTokens 的能力。当事情被阻塞时,“关闭”场景并不总是正确考虑,但 IMO 这样做是正确的。

编辑 3:根据要求,提供如何使用 BlockingCollection 的示例。我使用了foreachGetConsumingEnumerable 来使问题的消费者方面更加紧凑:

using System.Collections.Concurrent;
private static void testMethod()
{
  BlockingCollection<Action> myActionQueue = new BlockingCollection<Action>();
  var consumer = Task.Run(() =>
  {
    foreach(var item in myActionQueue.GetConsumingEnumerable())
    {
      item(); // Run the task
    }// Exits when the BlockingCollection is marked for no more actions
  });

  // Add some tasks
  for(int i = 0; i < 10; ++i)
  {
    int captured = i; // Imporant to copy this value or else
    myActionQueue.Add(() =>
    {
      Console.WriteLine("Action number " + captured + " executing.");
      Thread.Sleep(100);  // Busy work
      Console.WriteLine("Completed.");
    });
    Console.WriteLine("Added job number " + i);
    Thread.Sleep(50);
  }
  myActionQueue.CompleteAdding();
  Console.WriteLine("Completed adding tasks.  Waiting for consumer completion");

  consumer.Wait();  // Waits for consumer to finish
  Console.WriteLine("All actions completed.");
}

我在 Sleep() 调用中添加了内容,以便您可以看到添加的内容同时消耗了其他内容。您还可以选择启动任意数量的consumer lambda(只需将其称为Action,然后多次启动Action)或添加循环。您可以随时在集合上调用Count 以获取未运行的任务数。大概如果它不为零,那么您的生产者任务正在运行。

【讨论】:

  • 如果您将 BlockingCollection 与 ConcurrentQueue(及其方法 GetConsumingEnumerable)一起使用 - 它会在删除时阻塞。
【解决方案2】:

这个怎么样

void Main()
{
    var executor = new MyExecutor();
    executor.Execute(()=>Console.WriteLine("Hello"));
    executor.Execute(()=>Console.WriteLine(","));
    executor.Execute(()=>Console.WriteLine("World"));
}

public class MyExecutor
{
    private Task _current = Task.FromResult(0);

    public void Execute(Action action)
    {
        _current=_current.ContinueWith(prev=>action());
    }
}

UPD

更新的代码。现在我们可以获取动作的数量,从不同的线程推送等等。

void Main()
{
    var executor = new MyExecutor();
    executor.Execute(() => Console.WriteLine("Hello"));
    executor.Execute(() => Thread.Sleep(100));
    executor.Execute(() => Console.WriteLine(","));
    executor.Execute(() => { throw new Exception(); });
    executor.Execute(() => Console.WriteLine("World"));
    executor.Execute(() => Thread.Sleep(100));

    executor.WaitCurrent();

    Console.WriteLine($"{nameof(MyExecutor.Total)}:{executor.Total}");
    Console.WriteLine($"{nameof(MyExecutor.Finished)}:{executor.Finished}");
    Console.WriteLine($"{nameof(MyExecutor.Failed)}:{executor.Failed}");
}

public class MyExecutor
{
    private Task _current = Task.FromResult(0);
    private int _failed = 0;
    private int _finished = 0;
    private int _total = 0;
    private object _locker = new object();

    public void WaitCurrent()
    {
        _current.Wait();        
    }

    public int Total
    {
        get { return _total; }
    }

    public int Finished
    {
        get { return _finished; }
    }

    public int Failed
    {
        get { return _failed; }
    }

    public void Execute(Action action)
    {
        lock (_locker) // not sure that lock it is the best way here
        {
            _total++;
            _current = _current.ContinueWith(prev => SafeExecute(action));
        }
    }

    private void SafeExecute(Action action)
    {
        try
        {               
            action();
        }
        catch 
        {
            Interlocked.Increment(ref _failed);
        }
        finally 
        {
            Interlocked.Increment(ref _finished);
        }
    }
}

【讨论】:

  • 这似乎可行,只要您只从一个线程添加Tasks。在 OP 的情况下这很好,但我不认为它可以从多个“生产者”线程中工作,而且你不会得到 OP 想要查看有多少作业排队的愿望。编辑:另外,如果任务完成会发生什么,新添加的 ContinueWith Task 会立即执行吗?
  • @KevinAnderson 更新了答案。 It 任务如果完成,将立即开始继续。
  • 有趣的补充,但你的 _total 变量在任务开始运行之前不会更新,所以我认为这不会给他他已经排队的 Tasks 的数量.基本上,经过所有这些努力,我认为来自@Evk 的BlockingCollectionActions 或Tasks 的建议更清晰。但其他人可能不同意。
  • @KevinAnderson 是的,我更新了答案,谢谢。当然,在 .NET 中嵌入了一些东西可以让我们做同样的事情,那么使用它会更好,但我不确定阻塞收集 + 并发队列解决方案会更容易,但很高兴看到这样的示例 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-01
  • 1970-01-01
  • 2012-05-30
  • 1970-01-01
相关资源
最近更新 更多