【问题标题】:What is the Haskell response to Node.js?Haskell 对 Node.js 的响应是什么?
【发布时间】:2011-04-20 07:30:59
【问题描述】:

我相信 Erlang 社区并不羡慕 Node.js,因为它本身就提供非阻塞 I/O,并且可以轻松地将部署扩展到多个处理器(Node.js 中甚至没有内置的东西)。更多详情请访问http://journal.dedasys.com/2010/04/29/erlang-vs-node-jsNode.js or Erlang

Haskell 呢? Haskell 能否提供 Node.js 的一些好处,即无需求助于多线程编程即可避免阻塞 I/O 的干净解决方案?


Node.js 有很多吸引人的地方

  1. 事件:无线程操作,程序员只提供回调(如在 Snap 框架中)
  2. 回调保证在单线程中运行:不可能出现竞争条件。
  3. 漂亮而简单的 UNIX 友好 API。奖励:出色的 HTTP 支持。 DNS 也可用。
  4. 默认情况下,每个 I/O 都是异步的。这使得更容易避免锁定。但是,回调中过多的 CPU 处理会影响其他连接(在这种情况下,任务应拆分为更小的子任务并重新调度)。
  5. 客户端和服务器端的语言相同。 (不过,我看不出这个有太多价值。jQuery 和 Node.js 共享事件编程模型,但其余的却非常不同。我只是看不出如何在服务器端和客户端之间共享代码在实践中很有用。)
  6. 所有这些都打包在一个产品中。

【问题讨论】:

  • 我认为你应该在Programmers 上问这个问题。
  • 不包含一段代码并不构成主观问题。
  • 我对 node.js 了解不多,但是你的问题让我印象深刻:为什么你觉得线程的前景如此不愉快?线程应该是多路复用 I/O 的正确解决方案。我在这里广泛使用术语线程,包括 Erlang 的进程。也许您担心锁和可变状态?您不必这样做 - 如果对您的应用程序更有意义,请使用消息传递或事务。
  • @gawi 我不认为这听起来很容易编程 - 如果没有抢占,你必须处理饥饿和长延迟的可能性。基本上,线程是 Web 服务器的正确抽象——不需要处理异步 I/O 以及随之而来的所有困难,只需在线程中完成即可。顺便说一句,我写了一篇关于 Haskell 中 Web 服务器的论文,你可能会觉得很有趣:haskell.org/~simonmar/papers/web-server-jfp.pdf
  • "回调保证在单线程中运行:不可能出现竞争条件。"错误的。您可以轻松地在 Node.js 中设置竞争条件;只需假设一个 I/O 操作将在另一个 I/O 操作之前完成,然后 BOOM。 确实不可能的是一种特殊的竞争条件,即并发非同步访问内存中的同一字节。

标签: multithreading haskell concurrency node.js


【解决方案1】:

好的,所以看了一点@gawi 指出的node.js presentation,我可以多说一点关于 Haskell 与 node.js 的比较。在演讲中,Ryan 描述了 Green Threads 的一些好处,但接着说他并不认为缺少线程抽象是一个缺点。我不同意他的立场,尤其是在 Haskell 的上下文中:我认为线程提供的抽象对于使服务器代码更容易正确和更健壮至关重要。特别是:

  • 每个连接使用一个线程可以让您编写表达与单个客户端通信的代码,而不是编写同时处理所有个客户端的代码。可以这样想:使用线程处理多个客户端的服务器看起来与处理单个客户端的服务器几乎相同;主要区别在于前者的某处有一个fork。如果您要实现的协议非常复杂,那么同时为多个客户端管理状态机会变得非常棘手,而线程让您只需编写与单个客户端的通信即可。代码更容易正确,也更容易理解和维护。

  • 单个操作系统线程上的回调是协作式多任务处理,而不是抢占式多任务处理,这是线程所获得的。协作多任务的主要缺点是程序员负责确保没有饥饿。它失去了模块化:在一个地方犯了一个错误,它可能会搞砸整个系统。这确实是您不想担心的事情,抢占是简单的解决方案。此外,回调之间的通信是不可能的(它会死锁)。

  • Haskell 中的并发性并不难,因为大多数代码都是纯代码,因此在构造上是线程安全的。有简单的通信原语。在 Haskell 中使用并发比使用具有不受限制的副作用的语言更难自爆。

【讨论】:

  • 好的,所以我知道 node.js 可以解决两个问题:1- 在大多数语言中并发很难,2- 使用 OS 线程很广泛。 Node.js 的解决方案是使用基于事件的并发(w/libev)来避免线程之间的通信并避免 OS 线程的可伸缩性问题。由于纯度,Haskell 没有问题#1。对于 #2,Haskell 具有轻量级线程 + 事件管理器,最近在 GHC 中针对大规模上下文进行了优化。此外,对于任何 Haskell 开发人员来说,使用 Javascript 都不能被视为一个加分项。对于一些使用 Snap 框架的人来说,Node.js “很糟糕”。
  • 请求处理大部分时间是一系列相互依赖的操作。我倾向于同意为每个阻塞操作使用回调可能很麻烦。线程比回调更适合这个。
  • 是的! GHC 7 中全新的 I/O 多路复用使得在 Haskell 中编写服务器更加出色。
  • 你的第一点对我来说没有多大意义(作为一个局外人)......在 node.js 中处理请求时,您的回调处理单个客户端。只有在扩展到多个进程时才需要担心管理状态,即便如此,使用可用库也很容易。
  • 这不是一个单独的问题。如果这个问题是在 Haskell 中真正寻找工作的最佳工具,或者检查 Haskell 中是否存在出色的工作工具,那么需要挑战多线程编程不合适的隐含假设,因为 Haskell 确实正如唐斯图尔特所指出的那样,线程完全不同。解释为什么 Haskell 社区也不嫉妒 Node.js 的答案非常适合这个问题。 gawi 的回答表明这是对他问题的适当回答。
【解决方案2】:

Haskell 能否提供 Node.js 的一些好处,即无需求助于多线程编程即可避免阻塞 I/O 的简洁解决方案?

是的,事实上事件和线程在 Haskell 中是统一的。

  • 您可以在显式轻量级线程中编程(例如,一台笔记本电脑上的数百万个线程)。
  • 或;您可以基于可扩展的事件通知,以异步事件驱动的方式进行编程。

线程实际上是implemented in terms of events,并跨多个内核运行,具有无缝线程迁移、记录的性能和应用程序。

例如对于

32 核上的并发集合 nbody

在 Haskell 中,您有事件和线程,因为它是所有事件的底层。

Read the paper 描述实现。

【讨论】:

  • 谢谢。我需要消化这一切……这似乎是 GHC 特有的。我想没关系。 Haskell 语言有时是 GHC 可以编译的任何东西。以类似的方式,Haskell“平台”或多或少是 GHC 运行时。
  • @gawi:它以及所有其他捆绑到其中的软件包,因此开箱即用。这是我在 CS 课程中看到的相同图像;最好的部分是在 Haskell 中,在您自己的程序中实现类似的出色结果并不难。
  • 嗨,Don,您认为在回答此类问题时,您可以链接到性能最佳(Warp)的 haskell Web 服务器吗?这是针对 Node.js 的相当相关的基准:yesodweb.com/blog/2011/03/…
  • 只是理论上。 Haskell“轻量级线程”并不像你想象的那么轻量级。在 epoll 接口上注册回调比调度所谓的绿色线程便宜得多,它们当然比 OS 线程便宜,但它们不是免费的。创建 100.000 个使用大约。 350 MB 内存,需要一些时间。使用 node.js 尝试 100.000 个连接。完全没有问题。如果它不更快,那将是神奇的,因为 ghc 在引擎盖下使用 epoll,因此它们不能比直接使用 epoll 更快。不过,带有线程接口的编程非常好。
  • 另外:新的 IO 管理器 (ghc) 使用具有 (m log n) 复杂度的调度算法(其中 m 是可运行线程的数量,n 是线程的总数)。 Epoll 的复杂度为 k(k 是可读/可写 fd 的数量=。所以 ghc 的复杂度为 O(k * m log n)由 epoll 提供。让我们不要谈论 Windows 性能...... Node.js 更快,因为它使用 IOCP。
【解决方案3】:

首先,我不认为 node.js 正在做正确的事情来公开所有这些回调。你最终会用 CPS(延续传递风格)编写程序,我认为编译器的工作应该是进行这种转换。

事件:无线程操作,程序员只提供回调(如在 Snap 框架中)

因此,考虑到这一点,如果您愿意,您可以使用异步样式进行编写,但这样做您会错过以高效的同步样式编写,每个请求只有一个线程。 Haskell 在同步代码方面的效率非常高,尤其是与其他语言相比。下面都是事件。

回调保证在单个线程中运行:不可能出现竞争条件。

你仍然可以在 node.js 中出现竞争条件,但这更加困难。

每个请求都在它自己的线程中。当您编写必须与其他线程通信的代码时,由于 haskell 的并发原语,使其成为线程安全非常简单。

漂亮而简单的 UNIX 友好 API。奖励:出色的 HTTP 支持。 DNS 也可用。

看看 hackage 并亲自看看。

默认情况下,每个 I/O 都是异步的(但这有时会很烦人)。这使得更容易避免锁定。但是,回调中过多的 CPU 处理会影响其他连接(在这种情况下,应将任务拆分为更小的子任务并重新调度)。

您没有这样的问题,ghc 会将您的工作分配给真正的操作系统线程。

客户端和服务器端使用相同的语言。 (不过,我看不出这个有太多价值。JQuery 和 Node.js 共享事件编程模型,但其余的却非常不同。我只是看不出如何在服务器端和客户端之间共享代码在实践中很有用。)

Haskell 不可能在这里赢……对吧?再想一想,http://www.haskell.org/haskellwiki/Haskell_in_web_browser

所有这些都打包在一个产品中。

下载 ghc,启动 cabal。有一个满足各种需求的包。

【讨论】:

  • 我只是在扮演魔鬼的拥护者。所以,是的,我同意你的观点。除了客户端和服务器端的语言统一。虽然我认为它在技术上是可行的,但我认为它最终不会取代今天所有的 Javascript 生态系统(JQuery 和朋友们)。虽然这是 Node.js 支持者提出的一个论点,但我认为这不是一个非常重要的论点。您真的需要在表示层和后端之间共享这么多代码吗?我们的目标真的是让程序员只懂一种语言吗?
  • 真正的优势在于您可以在服务器端和客户端呈现页面,从而更轻松地创建实时页面。
  • @dan_waterworth 确切地说,请参阅 meteorderby.js
  • @gawi 我们有生产服务,其中 85% 的代码在客户端和服务器之间共享。这在社区中被称为通用 JavaScript。我们正在使用 React 在服务器上动态渲染内容,以减少在客户端首次有用渲染的时间。虽然我知道您可以在浏览器中运行 Haskell,但我不知道有任何一组“通用 Haskell”最佳实践允许使用相同的代码库进行服务器端和客户端渲染。
【解决方案4】:

我个人认为 Node.js 和使用回调编程是不必要的低级和有点不自然的事情。当一个好的运行时(例如 GHC 中的运行时)可以为您处理回调并且效率很高时,为什么还要使用回调进行编程?

与此同时,GHC 运行时已大大改进:它现在具有一个名为MIO 的“新的新 IO 管理器”,我相信其中“M”代表多核。它建立在现有 IO 管理器的基础上,其主要目标是克服 4+ 核性能下降的原因。本文提供的性能数据令人印象深刻。看看你自己:

借助 Mio,Haskell 中的真实 HTTP 服务器可扩展到 20 个 CPU 内核,与使用先前版本 GHC 的相同服务器相比,峰值性能高达 6.5 倍。 Haskell 服务器的延迟也得到了改善:[...] 在中等负载下,与以前版本的 GHC 相比,预期响应时间减少了 5.7 倍

还有:

我们还表明,借助 Mio,McNettle(用 Haskell 编写的 SDN 控制器)可以有效地扩展到 40 多个内核,在单台机器上达到每秒超​​过 2000 万个新请求的吞吐量,因此成为所有机器中最快的现有的 SDN 控制器。

Mio 已将其纳入 GHC 7.8.1 版本。我个人认为这是 Haskell 性能向前迈出的重要一步。比较以前 GHC 版本和 7.8.1 编译的现有 Web 应用程序性能会很有趣。

【讨论】:

    【解决方案5】:

    恕我直言,事件是好的,但通过回调进行编程却不是。

    使 Web 应用程序的编码和调试变得特别的大多数问题都来自于使它们具有可扩展性和灵活性的原因。最重要的是,HTTP 的无状态特性。这增强了可导航性,但这强加了控制反转,其中 IO 元素(在本例中为 Web 服务器)在应用程序代码中调用不同的处理程序。这个事件模型 - 或者更准确地说是回调模型 - 是一场噩梦,因为回调不共享变量范围,并且导航的直观视图丢失了。当用户来回导航时,很难防止所有可能的状态变化等问题。

    可以说,这些问题类似于事件模型工作正常的 GUI 编程,但 GUI 没有导航,也没有返回按钮。这使 Web 应用程序中可能的状态转换成倍增加。尝试解决这些问题的结果是具有复杂配置的重型框架,大量无处不在的魔术标识符,而没有质疑问题的根源:回调模型及其固有的变量范围共享不足,并且没有排序,因此序列必须通过链接标识符来构造。

    有一些基于顺序的框架,如 ocsigen (ocaml) seaside (smalltalk) WASH(已停产,Haskell)和 mflow (Haskell),它们解决了状态管理问题,同时保持了可导航性和 REST 丰富性。在这些框架中,程序员可以将导航表示为命令式序列,其中程序在单个线程中发送页面并等待响应,变量在范围内,后退按钮自动工作。这本质上会产生更短、更安全、更易读的代码,其中导航对程序员来说是清晰可见的。 (公平警告:我是 mflow 的开发者)

    【讨论】:

    • 在 node.js 中,回调用于处理异步 I/O,例如到数据库。你在谈论一些不同的东西,虽然很有趣,但不能回答问题。
    • 你是对的。花了三年的时间才得到一个答案,我希望能满足你的反对意见:github.com/transient-haskell
    • Node 现在支持异步函数,这意味着您可以编写实际上是异步的命令式代码。它在底层使用了 Promise。
    【解决方案6】:

    这个问题非常荒谬,因为 1) Haskell 已经以更好的方式解决了这个问题,并且 2) 以与 Erlang 大致相同的方式解决了这个问题。这是针对节点的基准:http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks

    给 Haskell 4 个核心,它可以在单个应用程序中每秒执行 100k(简单)请求。 Node 做不了那么多,也不能跨核心扩展单个应用程序。而且你不需要做任何事情来获得这个,因为 Haskell 运行时是非阻塞的。唯一在运行时中内置了非阻塞 IO 的其他(相对常见的)语言是 Erlang。

    【讨论】:

    • 荒谬?问题不是“Haskell 有响应”,而是“Haskell 响应是什么”。在提出这个问题的时候,GHC 7 甚至还没有发布,所以 Haskell 还没有“参与其中”(除了可能使用像 Snap 这样的 libev 的框架)。除此之外,我同意。
    • 我不知道您发布此答案时是否属实,但实际上现在有节点模块允许节点应用程序轻松跨内核扩展。此外,该链接将在单核上运行的 node.js 与在 4 核上运行的 haskell 进行比较。我希望看到它以更公平的配置再次运行,但可惜的是,github repo 不见了。
    • Haskell 使用超过 4 个内核会降低应用程序的性能。有一篇关于这个问题的论文,它正在积极研究,但它仍然是一个问题。因此,在 16 个核心服务器上运行 16 个 Node.js 实例很可能比使用 +RTS -N16 的单个 ghc 应用程序要好得多,因为这个运行时错误,它确实会比 +RTS -N1 慢。这是因为他们只使用一个 IOManager,当与许多 OS 线程一起使用时,它会变慢。我希望他们能修复这个错误,但它一直存在,所以我没有太大希望......
    • 查看此答案的任何人都应该知道,Node 可以轻松地在单个内核上处理 100k 个简单请求,并且跨多个内核扩展无状态 Node 应用程序非常容易。 pm2 -i max path/to/app.js 将根据可用内核自动扩展至最佳实例数。此外,Node 默认也是非阻塞的。
    【解决方案7】:

    【讨论】:

    • 这如何回答这个问题?
    • @dfeuer 该链接必须为,Snap Haskell Web Framework 已删除 libev,我不知道为什么格式化失败。节点服务器运行时一开始就是关于 Linux libev 的,Snap Web FrameWork 也是如此。带有 Snap 的 Haskell 就像带有 nodejs 的 ECMAscript,因此 Snap 如何与 nodejs 一起发展比 Haskell 更相关,在这种情况下可以更恰当地与 ECMAscript 进行比较。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-05
    • 2014-02-25
    • 2013-01-08
    • 1970-01-01
    相关资源
    最近更新 更多