【问题标题】:Design: Task-Asynchronous Pattern (TAP with await / async), vs threads with signalling vs other thread structures设计:任务异步模式(带有等待/异步的 TAP)、带有信号的线程 vs 其他线程结构
【发布时间】:2014-10-13 03:27:49
【问题描述】:

非常感谢您提供重新设计以下 C# 程序的想法。我试图在使用 1) TAP 实现多线程、2) 包含在其布尔值设置为 false 时终止的微调器的粗粒度线程或 3) 使用信号而不是这些布尔值的相同线程之间进行选择。我将在下面解释程序,以使情况清楚。

计划

该程序是 C# 中的游戏自动化应用程序,我正在开发它作为更好地学习语言和 C# (5.0) 功能的有趣方式。它的 UI 需要在应用程序运行时保持响应。

当打开 UI 中的特定选项卡时,程序会启动一个名为“Scan”的新线程,该线程在另一个类的新方法中扫描各种内存位置并使用这些快速变化的值更新 UI 中的标签UI 的 SynchronizationContext。这会在 while(scanning) 循环中进行,只要扫描 bool 为真(通常是程序的整个生命周期)。

当用户单击此选项卡上的“开始”按钮时,程序会启动两个执行以下操作的新线程: 线程“运行”沿着特定路径移动角色。线程“Action”在玩家运行路径的同时点击特定按钮并执行操作。如果出现某种场景,程序应该暂时停止正在运行的线程和动作线程,运行一个方法,当它完成后,再回到运行和动作。

当用户单击此选项卡上的停止按钮时,自动化应该停止并且线程终止。

挑战

我已经创建了一个工作版本,在每个线程中使用连续的微调器循环来处理各种工作。微调器使用一段时间(myBool)运行。对于三个线程,布尔值是:扫描、运行和操作。

当我想停止线程时,我将 bool 设置为 false,并使用 Thread.Join 等待线程正常终止,然后再继续。如上所述,线程可以由用户单击停止按钮来停止,或者作为其功能的一部分由程序自动停止。在后一种情况下,线程被停止、加入,然后在稍后的阶段重新启动。

在对 C# 5.0 中的线程和新的异步编程工具进行了大量阅读和研究之后,我意识到我目前的做法可能非常笨拙和不专业。它产生了很多同步/线程安全问题,所有这些的目标是更多地了解 C# 我想知道我是否应该将它改为细粒度的异步编程方法,使用 TAP适当的异步和等待。

这听起来像是带有取消令牌的任务可能有用的情况吗?线程毕竟是长时间运行的操作,所以我担心使用线程池(Task.Run)会导致线程池的卫生状况不佳(超额订阅)。如果异步编程在这里看起来不合适,那么像我所做的那样使用线程,而是使用信号来启动和停止线程呢?

任何想法都非常感谢。

【问题讨论】:

  • 您是否考虑过使用 TPL DataStream 或 ReactiveExtensions?两者都是为并发和/或异步数据流水线设计的。

标签: c# multithreading asynchronous async-await unmanaged


【解决方案1】:

没有。 TPL 旨在运行较短的任务,在这些任务中,新线程的分配总是会损害性能。它有很多不错的功能,比如作业队列和工作窃取(一个 TPL 线程可以从另一个线程获取作业)。它当然可以有更长的运行任务,但你不会从中获得太多好处。相反,你强制 TPL 分配新线程。

但是,这个问题有点笼统,因为我们需要有关您的实际实现的更多信息才能知道您应该使用什么。对于 Scan 线程,很明显它应该在单个线程中运行。

但对于其他人来说,这很难知道。它们是一直工作还是定期工作?如果它们一直在工作,您应该将它们放在单独的线程中。

至于线程同步还有另一种选择。您可以使用ConcurrentQueue 将所有必须绘制的内容排队。这样您就不需要任何同步。只需让 UI 线程检查队列并在其中绘制任何内容,而生产者可以继续工作。

实际上,通过这种方式,您可以将与 UI 绘制无关的任何内容移动到其他线程。这也应该可以提高您的应用程序的响应能力。

public void ActionRunnerThreadFunc()
{
    _drawQueue.Enqueue(new SpaceShipRenderer(x, y));
}

public void UIThreadFunc()
{
    IItemRender item;
    if (_drawQueue.TryDequeue(out item))
        item.Draw(drawContext);
}

【讨论】:

  • 感谢 J 的回答。该应用实际上并不是一款游戏,而是一种自动化游戏玩法的工具(因此可以移动角色并执行各种任务,无需人工干预)。我认为任务似乎很有用,因为它们可以有一个返回类型,并且提供了附加延续的可能性(显式地使用等待者或隐式地通过 await 关键字)。所以我认为你是对的,内存扫描器应该只是在一个单独的线程上运行,并且可以使用 ManualResetEvent.WaitOne 而不是扫描器 bool。 (在下一条评论中继续)
  • 但是对于另外两个线程:它们不会永远运行。可以说我想做的是砍树。所以一个线程让角色在设定的路径中跑来跑去。另一个并行运行的线程不断地点击一个按钮来寻找树。一旦找到一棵树,这两个线程都应该暂时停止,并且应该启动 treeChopping 方法。砍完树后,玩家应该回到路上寻找树木。感觉这三个任务在这里会很好用吧?
  • 如果树木切割在一秒钟内完成,我将在任务中运行它。否则我会把它放在一个单独的线程中。
  • 但是仅仅使用TaskCreationOptions.LongRunning 来避免线程池呢?您认为这里不使用任务的原因是什么?
  • “最小惊讶原则”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-07-13
  • 1970-01-01
  • 1970-01-01
  • 2021-08-30
  • 2020-07-08
  • 2013-02-15
  • 1970-01-01
相关资源
最近更新 更多