英文:
How the "actor model" resolve the "shared state" problem?
问题
在"actor model"中是否有一些方法可以避免"共享状态"?但我找不到一个好的例子,用Erlang/Elixir来展示它。
在《Programming Erlang》第二版第22章中有一个"withdraw"的例子,但这个例子似乎是展示如何编写选项(opt),而不是如何处理"共享状态"。它使用ETS数据库来保存"余额",因此ETS是"共享状态",并且它只使用一个进程,而不是两个来"withdraw"和"deposit"。
所以是否有一些好的"withdraw"的例子来展示Erlang/Elixir如何处理"共享状态"的问题?我认为它必须在消息中编码余额来处理它,并且需要在所有地方传递"余额",以避免在固定的地方共享它。也许Haskell的MVar会解决这个问题。
英文:
It seems the "withdraw" porg is the classic example,it's used in 《sicp》 and 《design concepts in programming languages》to explain the "shared state"
I want to know in the "actor model" is there some method to aovid the "shared state"? But I can't find a good example write in erlang/elixir to show it
There is an example of withdraw in 《programming erlang》2ed,chapter 22,but the example seems to show how to write the opt,not how to deal the "shared state":it use ets database to save the "balance",so the ets is the "shared state",and it use only one process,not two to "withdraw" and "deposit"
So is there some good example of "withdraw" to show how erlang/elixir deal with the "shared state" problem?I think it have to encode the balance in the message to handle it,and pass the "balance" everywhere,to aovid share it in a fix place.Maybe haskell's MVar will resolve it
答案1
得分: 2
演员,或者Erlang/Elixir进程,实际上是一个单线程。如果你在GenServer的handle_call
函数中,可以保证在完成这个特定消息处理程序之前,你不会收到另一条消息或调用另一个handle_call
。所有发送给进程的消息按某种顺序接收并逐个处理;在进程内部没有并发,因此没有机会同时修改状态。
一个最简单的Elixir设置可能如下所示:
defmodule Account do
use GenServer
def start_link(balance) do
GenServer.start_link(__MODULE__, balance)
end
def deposit(account, amount) do
GenServer.call(account, {:deposit, amount})
end
def withdraw(account, amount) do
GenServer.call(account, {:withdraw, amount})
end
@impl true
def init(balance) do
{:ok, balance}
end
@impl true
def handle_call({:deposit, amount}, _, balance) do
new_balance = balance + amount
{:reply, :ok, new_balance}
end
@impl true
def handle_call({:withdraw, amount}, _, balance) do
if amount < balance do
{:reply, {:error, :insufficient_balance}, balance}
else
new_balance = balance - amount
{:reply, :ok, new_balance}
end
end
end
在具有可变状态的传统多线程环境中,一个线程有机会计算new_balance
,而另一个线程覆盖现有的余额,从而可能导致数据丢失(你引用了《计算机程序的结构与解释》(Structure and Interpretation of Computer Programs)中的问题描述)。但由于演员是单线程的,即使多个其他进程在同一个账户上调用Account.withdraw/2
,你可以确保获得一致的行为。
英文:
An actor, or an Erlang/Elixir process, is in effect a single thread. If you're in a GenServer's handle_call
function you are guaranteed to not receive another message or invoke another handle_call
until this particular message handler is complete. All messages sent to a process are received in some order and handled one at a time; there is no concurrency within a process and so no opportunity for state to be concurrently modified.
A minimal Elixir setup might look like
defmodule Account do
use Genserver
def start_link(balance) do
GenServer.init(__MODULE__, balance)
end
def deposit(account, amount) do
GenServer.call(account, {:deposit, amount})
end
def withdraw(account, amount) do
GenServer.call(account, {:withdraw, amount})
end
@impl true
def init(balance) do
{:ok, balance}
end
@impl true
def handle_call({:deposit, amount}, _, balance) do
new_balance = balance + amount
{:reply, :ok, new_balance}
end
@impl true
def handle_call({:withdraw, amount}, _, balance) do
if amount < balance do
{:reply, {:error, :insufficient_balance}, balance}
else
new_balance = amount - balance
{:reply, :ok, new_balance}
end
end
end
In a classical multi-threaded environment with mutable state, you have an opportunity for one thread to calculate a new_balance
while another thread overwrites the existing balance, and changes can get lost. (You cite Structure and Interpretation of Computer Programs and it has an entire subsection describing the issues here.) But since the actor is single-threaded, even if multiple other processes call Account.withdraw/2
on the same account, you're guaranteed to get a consistent behavior.
答案2
得分: 1
仅翻译代码和相关内容:
-
Just to add to what David Maze explained: a process sends a message to an OTP genserver by calling the function:
仅添加一下David Maze所解释的内容:进程通过调用以下函数向OTP genserver发送消息:
gen_server:call(GenserverModuleName, Message)
-
When processA calls that function, a message is sent to the genserver process, for example in Chapter 22 the message might be a withdrawal:
{remove, "account0001", 200}
.当processA调用该函数时,会向genserver进程发送一条消息,例如在第22章中,该消息可能是一笔取款:
{remove, "account0001", 200}
。 -
When processB calls that function, another message is sent to the genserver process, e.g. another withdrawal:
{remove, "account001", 1000}
.当processB调用该函数时,另一条消息被发送到genserver进程,例如另一笔取款:
{remove, "account001", 1000}
. -
The genserver process, like all erlang processes, has a mailbox that accumulates messages from all the processes that send it messages.
genserver进程,就像所有Erlang进程一样,有一个邮箱,累积了所有发送消息的进程的消息。
-
The genserver then searches through the mailbox for messages that it knows how to handle, e.g. messages that match the parameters specified in the various clauses of the
handle_call()
function definition.然后,genserver会搜索邮箱中的消息,寻找它知道如何处理的消息,例如与
handle_call()
函数定义中的各个子句中指定的参数匹配的消息。 -
However, a genserver only works on one message at a time, therefore there can be no race condition, i.e. where two processes try to change the same piece of data at the same time, like the account balance.
但是,genserver一次只能处理一条消息,因此不会出现竞争条件,即两个进程尝试同时更改相同的数据,比如账户余额。
-
The genserver will handle one withdrawal message, and if the account has a big enough balance, then the withdrawal is allowed, and the balance is updated in the ets table.
genserver会处理一条取款消息,如果账户余额足够大,那么允许取款,并在ets表中更新余额。
-
Then the genserver will handle the next withdrawal message, and if the new balance is sufficiently large, the second withdrawal is allowed, and the balance is updated in the ets table.
然后genserver会处理下一条取款消息,如果新余额足够大,那么允许第二笔取款,并在ets表中更新余额。
-
In other words, the genserver does not spin off two processes to handle the two withdrawal messages concurrently, rather the genserver handles the two withdrawal messages sequentially.
换句话说,genserver不会同时启动两个进程来处理两条取款消息,而是顺序处理这两条取款消息。
-
it use ets database to save the "balance", so the ets is the "shared state
它使用ets数据库来保存"balance",所以ets是"shared state"。
-
I think it have to encode the balance in the message to handle it, and pass the "balance" everywhere, to avoid sharing it in a fixed place.
我认为它必须在消息中编码余额以处理它,并将"balance"传递到各处,以避免在一个固定的位置共享它。
-
No, the balance can remain in the ets table for the reasons stated above.
不,根据上面的原因,余额可以继续留在ets表中。
英文:
Just to add to what David Maze explained: a process sends a message to an OTP genserver by calling the function:
gen_server:call(GenserverModuleName, Message)
When processA calls that function, a message is sent to the genserver process, for example in Chapter 22 the message might be a withdrawal: {remove, "account0001", 200}
. When processB calls that function, another message is sent to the genserver process, e.g. another withdrawal: {remove, "account001", 1000}
. The genserver process, like all erlang processes, has a mailbox that accumulates messages from all the processes that send it messages.
The genserver then searches through the mailbox for messages that it knows how to handle, e.g. messages that match the parameters specified in the various clauses of the handle_call()
function definition. However, a genserver only works on one message at a time, therefore there can be no race condition, i.e. where two processes try to change the same piece of data at the same time, like the account balance. The genserver will handle one withdrawal message, and if the account has a big enough balance, then the withdrawal is allowed, and the balance is updated in the ets table. Then the genserver will handle the next withdrawal message, and if the new balance is sufficiently large, the second withdrawal is allowed, and the balance is updated in the ets table. In other words, the genserver does not spin off two processes to handle the two withdrawal messages concurrently, rather the genserver handles the two withdrawal messages sequentially.
> it use ets database to save the "balance",so the ets is the "shared
> state
The genserver is the only process that knows about the ets table, and the genserver only accesses the ets table sequentially.
> I think it have to encode the balance in the message to handle it,and
> pass the "balance" everywhere,to aovid share it in a fix place.
No, the balance can remain in the ets table for the reasons stated above.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论