【问题标题】:C# first class continuation via C++ interop or some other way?通过 C++ 互操作或其他方式进行 C# 第一类延续?
【发布时间】:2012-01-30 22:20:18
【问题描述】:

我们有一个非常高性能的多任务处理、近乎实时的 C# 应用程序。这种性能主要是通过使用本地调度程序在内部实施协作多任务来实现的。这通常称为微线程。在这个系统中,所有任务都通过队列与其他任务进行通信。

我们遇到的具体问题似乎只能通过 C# 不支持的第一类延续来解决。

特别是在处理队列的两种情况下会出现问题。每当任何特定任务在将项目放入队列之前执行一些工作时。队列满了怎么办?

相反,一个不同的任务可能会做一些工作,然后需要从队列中取出一个项目。如果该队列为空怎么办?

我们已经在 90% 的情况下解决了这个问题,方法是将队列链接到任务,以避免任务在出站队列已满或入站队列为空时被调用。

此外,某些任务被转换为状态机,因此它们可以在队列满/空时进行处理,并且无需等待即可继续。

真正的问题出现在一些极端情况下,其中任何一种解决方案都不切实际。这种情况下的想法是在该点保存堆栈状态并切换到不同的任务,以便它可以完成工作,然后在能够继续时重试等待的任务。

过去,我们试图让等待任务回调到调度中(递归),以允许其他任务稍后重试等待任务。然而,这导致了太多的“死锁”情况。

有一个自定义 CLR 主机的示例,使 .NET 线程实际上作为“光纤”运行,这实际上允许在线程之间切换堆栈状态。但现在我似乎找不到任何示例代码。此外,似乎需要一些相当复杂的事情才能让它正确。

有没有人有其他的创意想法如何在任务之间高效切换并避免上述问题?

是否有任何其他 CLR 主机提供此功能(商业或其他方式)?是否有任何附加的原生库可以为 C# 提供某种形式的延续?

【问题讨论】:

  • 光纤模式 CLR 主机示例 -- 你可能指的是 Dino Viehland 的博客系列,从这里开始:blogs.msdn.com/b/dinoviehland/archive/2004/08/16/215140.aspx
  • Nit:死锁不是效率问题。这是一个正确性问题。 (另外,一个完整的未绑定队列和没有更多内存有什么区别?状态就是状态,必须存储在某个地方。)
  • 我们在 C# 5 中添加了一种延续形式。虽然它们不完全是一流的调用/cc 风格的延续,但它们在道德上是等价的。查看“异步 CTP”的预览版。 msdn.microsoft.com/en-us/vstudio/gg316360.aspx。另请参阅 Stephen Toub 最近关于异步功能的性能特征的 MSDN 杂志文章。
  • @Wayne,异步 CTP 确实包含一些调度程序,但您不必使用它们。当你写 await something, something 时,它会得到延续,这取决于它如何处理。所以,在我看来你只需要实现你自己的等待类型。
  • Mono 的 Continuations 似乎就是这样:一个 CLI VM 扩展,允许将任务“恢复”到给定状态。他们甚至有一个带有自己的调度程序类的微线程库。

标签: c# .net queue multitasking continuations


【解决方案1】:

解决问题的方法是使用无锁算法,允许系统范围内至少完成一项任务。您需要使用依赖于 CPU 的内联汇编程序来确保原子 CAS(比较和交换)。维基百科有 article 以及 Douglas Schmidt 描述的 book 模式,称为“面向模式的软件架构,并发和网络对象的模式”。我不清楚你将如何在 dotnet 框架下做到这一点。

解决问题的其他方法是使用发布-订阅者模式或可能的线程池。

希望这有帮助吗?

【讨论】:

  • 谢谢,但 CAS 的测试也很慢。为了使每个 CAS 有效,所有核心都必须同步。我们的策略现在使用零锁,其中包括像 CAS 这样的零自旋锁。相反,不同的组件通过类似于 erlang 设计的消息传递进行通信。这允许真正的并行处理,而不会出现缓存未命中或 CAS 减慢。
【解决方案2】:

C# 5 CTP,它对使用新的async 关键字声明的方法执行连续传递式转换,并在使用await 关键字时执行基于连续传递的调用。

这实际上不是一个新的 CLR 功能,而是一组指令,供编译器对您的代码执行 CPS 转换,以及一些用于操作和调度延续的库例程。 async 方法的激活记录放置在堆而不是堆栈上,因此它们不绑定到特定线程。

【讨论】:

  • 您可能需要注意,这是一个极其有限的 CPS 转换。
  • 缺乏消息传递的完整观察者模式的功能、性能和灵活性。消息传递似乎是多核和分布式编程最优雅、最有效的解决方案。
  • @Wayne 同意。我之所以提出它只是因为它是一流的语言功能(虽然在运行时级别不是一流的,因为它不能应用于不是为该功能编写和编译的代码)。
【解决方案3】:

不,不会工作。 C#(甚至 IL)是一种过于复杂的语言,无法以一般方式执行此类转换 (CPS)。你能得到的最好的就是 C# 5 所能提供的。也就是说,您可能无法使用更高阶的循环/迭代来中断/恢复,这是您真正想要的通用可重构延续。

【讨论】:

    【解决方案4】:

    由于压力问题,光纤模式已从 CLR 的 v2 中删除,请参阅:

    据我所知,尚未重新添加纤维支持,尽管通过阅读上述文章,它可能会再次添加(但事实上 6-7 年来没有任何关于该主题的提及让我相信这不太可能)。

    FYI 纤程支持旨在为使用纤程(例如 SQL Server)的现有应用程序提供一种方式来托管 CLR,以使它们能够最大限度地提高性能,不是作为一种方法允许 .Net 应用程序创建数百个线程 - 短纤不是解决问题的灵丹妙药,但是如果您有一个使用纤程的应用程序并希望托管 CLR,那么托管托管 API确实为 CLR 提供了与您的应用程序“很好地工作”的方法。一个很好的信息来源是managed hosting API documentation,或者查看 SQL Server 如何托管 CLR,其中有几篇信息丰富的文章。

    还可以快速阅读Threads, fibers, stacks and address space

    【讨论】:

      【解决方案5】:

      实际上,我们决定了一个方向。我们使用带有消息传递的观察者模式。我们构建了一个本地库来处理类似于 Erlang 进程的“代理”之间的所有通信。稍后我们将考虑使用 AppDomains 来更好地将代理彼此分开。设计思路借鉴了 Erlang 编程语言,具有极其可靠的多核和分布式处理。

      【讨论】:

      • 如果您对此感到好奇,您可以找到我在 stackoverflow 上专门针对 C# 中的 Erlang 样式处理的其他帖子。这些都是在寻求帮助以了解 Erlang 消息传递的优缺点以利用那里的设计理念时完成的。具体来说,队列满的问题在 Erlang 中得到了解决,现在借用这个问题以及按值通信的复制。
      • 有趣的是,我们现在构建的消息传递引擎会确定接收到的消息是否将在与发送者相同的线程/核心上运行,这样就完全消除了锁。因此,只有在绝对需要跳转到另一个内核时才会发生争用,这只会导致 L1 缓存未命中,这在四核上比 CAS 操作便宜 4 倍,因为 CAS 停止所有内核,而 L1 缓存未命中仅影响读取数据的单个内核那是写的。
      • 对于延续,我们只是使用状态机,因为无论如何编译器都是这样做的。可悲的是,C# 中没有任何用于延续的语法糖。但是状态机很好地服务于目的!幸运的是,系统中只有少数几个地方需要延续/状态机。
      猜你喜欢
      • 2015-06-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-10
      • 2016-05-15
      • 1970-01-01
      • 2012-06-09
      • 2012-06-09
      相关资源
      最近更新 更多