函数式编程中不可变数据的问题

7
我对函数式编程还不太熟悉。我所理解的是,函数式编程使用纯函数编写代码,不改变数据的值。
在函数式编程中,我们需要更新一个变量时,不会直接改变变量的值,而是创建一个新变量。
比如,我们有一个变量 x,代表程序发送的 HTTP 请求总数。如果有两个线程,我希望它们能在任何线程发出 HTTP 请求时都能增加 x 的值。但如果两个线程各自拷贝了变量 x,它们怎么才能同步变量 x 的值呢?例如:如果线程 1 发送了 10 个 HTTP 请求,线程 2 发送了 11 个 HTTP 请求,那么它们分别会打印出 10 和 11,但我该如何打印出 21 呢?

4
线程永远不是目标,而是工具。通常情况下,您希望尽可能地忘记它们,并且只让一些低级别、经过深思熟虑的机制在幕后为您处理线程。 - Erik Kaplun
6个回答

8
我可以提供关于 clojure 的解答。在 clojure 中,如果需要协调对共享状态的访问,语言中有一些构造被设计来处理这些情况。
在这种情况下,您可以使用一个atom来保存值。对atom所做的更改是原子性的,并将通过Clojure的STM进行乐观地完成。Atom 是 Clojure 引用类型之一,它本质上是一个对值的引用,可通过 atom 的变化函数以受控方式随时间改变。
请参阅 clojure 文档 以获取有关 atoms 和其他引用类型的更多信息。

1

我将解释Haskell部分。 MVar是线程间通信的机制之一。这是从Simon Marlow的书中摘取的一个例子(程序本身很容易理解):

main = do
  m <- newEmptyMVar
  forkIO $ do putMVar m 'x'; putMVar m 'y'
  r <- takeMVar m
  print r
  r <- takeMVar m
  print r

以上程序的输出将是:
'x'
'y'

你可以在上面的例子中看到,变量 m 中的 MVar 值是在线程之间共享的。你可以在这本书中了解更多关于这些技术的知识。

-1 是因为没有解决真正的问题——在像 Haskell 这样的语言中,如何正确使用线程的误解。 - Erik Kaplun
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Sibi

0

好的,我会尝试提供更一般化的解释,关于状态保持,因为我认为这才是你真正想知道的。

通常情况下,您可以通过递归来实现相同的功能,例如,如果您有以下函数:

somefun ()->
   somefun(0).
somefun (X) ->
  perform_http_request(),
  if(something!=quit)
     somefun(X+1)
end function.

generate_thread(0, Accumulator) ->
      Accumulator;
generate_thread(X, Accumulator) ->
      Y = somefun(),
      NewAccumulator = add_to_accumulator(Y),
      generate_thread(X-1, NewAccumulator).

我匆忙地输入了这个,这是一个非常通用的解释(你不能直接使用这个代码),但是你会发现在这里你实际上不需要可变性... 当所有线程完成处理时,函数将结束,现在实际的线程同步取决于您选择的语言,不同的语言有不同的并发处理和“线程”处理方式... 如果你对并发感兴趣,我建议你看看 Erlang,因为它有一个非常好的并发模型。

无论如何,在最后,你只需将返回的累加器中的所有值相加并显示出来,顺便看一下 foldl 和 foldr 函数。


0

我也会讲一下Haskell的部分。

首先,我想澄清一些事情:

在函数式编程中,当我们需要更新变量时,我们不是改变变量的值,而是创建新的变量。

这并不是很准确。在FP中,我们在需要时创建新的“变量”,而不是在需要改变现有变量时。当我们执行您所描述的操作时,我们甚至不会考虑变异的问题;我们可能只是认为我们正在创建一个类似于我们拥有的值的新值。

您在线程中描述的内容略有不同。您实际上正在寻找副作用(增加计数器)。 Haskell是纯的,不允许您随意抛出副作用而不明确说明。因此,在这种情况下,您将需要使用引用类型/可变单元。最简单的一个称为IORef,在这个意义上非常像一个变量;您可以分配一个值,读取当前值等等。

因此,正如您所看到的,当您寻找这些东西时,您确实只有一个计数器的副本。

以上是我的回答的精髓,但你具体询问了线程,所以我也会回答你。
IORef实际上并不是线程安全的。因此,建议使用MVar。它们不像普通变量,但很接近,并且可以优雅地完成工作。一般而言:它们抽象了变量和锁定。不过,我认为你可能会发现TVar更容易使用。它们的行为类似于IORef/变量,唯一的区别是它们是线程安全的;您可以将它们的操作组合成一个操作,并且对它们执行的任何操作都是原子操作(STM)。

顺便说一句,你可能会找到避免状态的方法,这是非常鼓励的。例如,您可以让两个线程执行一个异步递归函数,通过参数记住已经发出了多少请求,并稍后将其作为返回值。所有线程返回请求总数的总和。这可以避免对计数器的副作用,但只有在线程完成时才能给您产生结果。这有些局限性,因此有时您可能需要那个副作用。


0

我自己不是大师,但我认为你可能误解了。

如果不保留状态,你就无法创建一个非常有用的程序。必须以某种方式保留状态。FP 的目标不是避免状态,而是控制状态的使用

这样看待它,你的状态应该像数据库条目一样隔离和安全。如果你把状态视为对待数据库的方式,我想你会没问题的

这意味着,

  • 你不会像这样使用登录 (inc count)。相反,你将拥有一个函数 increment-count! 来安全地更新计数。注意 !,这意味着副作用。
  • 你不会有依赖于副作用的代码。相反,你将依赖于期望从参数中获取一切的函数。除非它们绝对需要依赖状态。比如更新计数,这是对状态的不可避免的调用。
  • 你的首选应该是避免状态。当将其传递给函数变得不可能时,你将创建适当更新的状态。
  • 将状态视为你处理外部 API 的方式。某种远程的东西,你必须使用某种协议来访问。

希望这有意义。


0

我将讲解Erlang部分的地址。即使在Erlang中,同步也没有什么魔法,某个地方必须处理同步。只是Erlang具有不可变性(又称无变量),有助于防止并发编程中常见的同步错误。Erlang/OTP像gen_server已经具备管理状态的基础设施。事实上,gen_server是单线程的,它接收到的任何消息都会排队在邮箱中。这里有一个关于Erlang消息并发的链接。How Erlang processes access mailbox concurrently

在原始帖子的情况下,为了钉住HTTP请求计数器,您可以使用单个gen_server OTP(Erlang)。您会惊讶于它可以处理多少吞吐量。如果单个gen_server的吞吐量确实不足,可以使用分层gen_server来聚合计数。Erlang/OTP配备了一组运行时API,以实时测量性能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接