在函数式编程中,我们需要更新一个变量时,不会直接改变变量的值,而是创建一个新变量。
比如,我们有一个变量 x,代表程序发送的 HTTP 请求总数。如果有两个线程,我希望它们能在任何线程发出 HTTP 请求时都能增加 x 的值。但如果两个线程各自拷贝了变量 x,它们怎么才能同步变量 x 的值呢?例如:如果线程 1 发送了 10 个 HTTP 请求,线程 2 发送了 11 个 HTTP 请求,那么它们分别会打印出 10 和 11,但我该如何打印出 21 呢?
我将解释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
值是在线程之间共享的。你可以在这本书中了解更多关于这些技术的知识。好的,我会尝试提供更一般化的解释,关于状态保持,因为我认为这才是你真正想知道的。
通常情况下,您可以通过递归来实现相同的功能,例如,如果您有以下函数:
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 函数。
我也会讲一下Haskell的部分。
首先,我想澄清一些事情:
在函数式编程中,当我们需要更新变量时,我们不是改变变量的值,而是创建新的变量。
这并不是很准确。在FP中,我们在需要时创建新的“变量”,而不是在需要改变现有变量时。当我们执行您所描述的操作时,我们甚至不会考虑变异的问题;我们可能只是认为我们正在创建一个类似于我们拥有的值的新值。
您在线程中描述的内容略有不同。您实际上正在寻找副作用(增加计数器)。 Haskell是纯的,不允许您随意抛出副作用而不明确说明。因此,在这种情况下,您将需要使用引用类型/可变单元。最简单的一个称为IORef
,在这个意义上非常像一个变量;您可以分配一个值,读取当前值等等。
因此,正如您所看到的,当您寻找这些东西时,您确实只有一个计数器的副本。
以上是我的回答的精髓,但你具体询问了线程,所以我也会回答你。IORef
实际上并不是线程安全的。因此,建议使用MVar
。它们不像普通变量,但很接近,并且可以优雅地完成工作。一般而言:它们抽象了变量和锁定。不过,我认为你可能会发现TVar
更容易使用。它们的行为类似于IORef
/变量,唯一的区别是它们是线程安全的;您可以将它们的操作组合成一个操作,并且对它们执行的任何操作都是原子操作(STM)。
顺便说一句,你可能会找到避免状态的方法,这是非常鼓励的。例如,您可以让两个线程执行一个异步递归函数,通过参数记住已经发出了多少请求,并稍后将其作为返回值。所有线程返回请求总数的总和。这可以避免对计数器的副作用,但只有在线程完成时才能给您产生结果。这有些局限性,因此有时您可能需要那个副作用。
我自己不是大师,但我认为你可能误解了。
如果不保留状态,你就无法创建一个非常有用的程序。必须以某种方式保留状态。FP 的目标不是避免状态,而是控制状态的使用。
这样看待它,你的状态应该像数据库条目一样隔离和安全。如果你把状态视为对待数据库的方式,我想你会没问题的。
这意味着,
(inc count)
。相反,你将拥有一个函数 increment-count!
来安全地更新计数。注意 !
,这意味着副作用。希望这有意义。
我将讲解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,以实时测量性能。