在Erlang中测试互斥

时间:2015-02-17 08:52:49

标签: erlang mutex mutual-exclusion

我目前有一个简单的银行帐户使用erlang编写,我也有相同的银行帐户重写,以便相互解释这个想法是这样的,以便在设置/获取余额可以被中断的情况下无法进行两次存款,以便结束值是错误的,例如bal A = 10 bal B = 20:

WRONG
get_bal.A 0 → get_bal.B 0 → set_bal.A 10 → set_bal.B 20 == 20
RIGHT
get_bal.A 0 → set_bal.A 10 → get_bal.B 10 → set_bal.B 30 == 30

我的代码初始代码如下:

-module(bank).
-export([account/1, start/0, stop/0, deposit/1, get_bal/0, set_bal/1]).

account(Balance) ->
receive
    {set, NewBalance} ->
        account(NewBalance);
    {get, From} ->
        From ! {balance, Balance},
        account(Balance);
    stop -> ok
end.

start() ->
    Account_PID = spawn(bank, account, [0]),
    register(account_process, Account_PID).

stop() ->
    account_process ! stop,
    unregister(account_process).

set_bal(B) ->
    account_process ! {set, B}.

get_bal() ->
    account_process ! {get, self()},
    receive
    {balance, B} -> B
end.

deposit(Amount) ->
    OldBalance = get_bal(),
    NewBalance = OldBalance + Amount,
    set_bal(NewBalance).

我们的想法是设置一个测试,以便在最终余额出错时我可以收到错误,如果按计划进行,则会收到错误。 我重写的代码如下:

account(Balance) ->
receive
    {deposit, Amount, From} ->
        NewBalance = Balance + Amount,
        From ! {deposit, Amount, NewBalance},
        account(NewBalance);
    {withdraw, Amount, From} when Amount > Balance ->
        From ! {error, {insufficient_funds, Amount, Balance}},
        account(Balance);
    {withdraw, Amount, From} ->
        NewBalance = Balance - Amount,
        From ! {withdrawal, Amount, NewBalance},
        account(NewBalance);    
    {get, From} ->
        From ! {balance, Balance},
        account(Balance);
    stop -> ok
end.

deposit(Amount) when Amount > 0 ->
account_process ! {deposit, Amount, self()},
receive
    {deposit, Amount, NewBalance} ->
        {ok, NewBalance}
end.

withdraw(Amount) when Amount > 0 ->
account_process ! {withdraw, Amount, self()},
receive
    {withdrawal, Amount, NewBalance} ->
        {ok, NewBalance};
    Error ->
        Error
end.

感谢阅读,非常感谢任何帮助。

4 个答案:

答案 0 :(得分:1)

在Erlang中,互斥不是问题。 进程是actor,它们之间没有共享内存。

看看这个问题:Is it easy to write traditional concurrency problems in Erlang?

至于代码,我可能会做这样的事情(" bank"表示为gen_server)。这不是解决您问题的方法,而是使用OTP实现相同目标的另一种方式:

-module(bank).

-behaviour(gen_server).

%% API
-export([start_link/0, new_account/1, withdraw/2, deposit/2, get_bal/1]).

%% gen_server callbacks
-export([init/1,
     handle_call/3,
     handle_cast/2,
     handle_info/2,
     terminate/2,
     code_change/3]).

-record(state, {accounts = [] :: list()}).

%%%===================================================================
%%% API
%%%===================================================================

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

new_account(Name) ->
    gen_server:call(?MODULE, {new_account, Name}).

deposit(Account, Amount) when Amount > 0 ->
    gen_server:call(?MODULE, {deposit, Account, Amount}).

withdraw(Account, Amount) when Amount > 0 ->
    gen_server:call(?MODULE, {withdraw, Account, Amount}).

get_bal(Account) ->
    gen_server:call(?MODULE, {get_bal, Account}).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

init([]) ->
    {ok, #state{}}.

handle_call({new_account, Name}, _From, State) ->
    Accounts = State#state.accounts,
    case find_account(Name, Accounts) of
        none ->
            {reply, {account_created, Name}, State#state{accounts=[{Name, 0}|Accounts]}};
        _ ->
            {reply, already_exists, State}
        end;

handle_call({get_bal, Account}, _From, State) ->
    Accounts = State#state.accounts,
    {_Name, Balance} = find_account(Account, Accounts),
    {reply, Balance, State};

handle_call({deposit, Account, Amount}, _From, State) ->
    Accounts = State#state.accounts,
    {Name, Balance} = find_account(Account, Accounts),
    NewBalance = Balance + Amount,
    NewAccounts = lists:keyreplace(Name, 1, Accounts, {Name, NewBalance}),
    {reply, {deposit, Amount, NewBalance}, State#state{accounts=NewAccounts}};

handle_call({withdraw, Account, Amount}, _From, State) ->
    Accounts = State#state.accounts,
    {Name, Balance} = find_account(Account, Accounts),
     case Amount of
        Amount when Amount > Balance ->
            {reply, {insufficient_funds, Amount, Balance}, State};
        _ ->
            NewBalance = Balance - Amount,
            NewAccounts = lists:keyreplace(Name, 1, Accounts, {Name, NewBalance}),
            {reply, {withdrawal, Amount, NewBalance}, State#state{accounts=NewAccounts}}
    end;

handle_call(_Request, _From, State) ->
    Reply = not_implemented,
    {reply, Reply, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
find_account(Account, Accounts) ->
    proplists:lookup(Account, Accounts).

答案 1 :(得分:1)

测试此方法的一种方法是让两个或多个银行客户端进程将其消息交错到银行。每个客户端进程都可以使用要发送到银行的消息列表进行初始化,然后每个客户端进程都处于循环中,等待来自某个控制器进程的消息,告诉它将其下一条消息发送给银行。控制器将充当一个门,告诉每个客户轮流发送它的下一条消息,这样整体效果就是客户端消息会混合在一起。

如果您有两个这样的客户受到这样的控制,如果客户A想要存入10并且客户B希望存入20,那么如果他们使用您的原始银行代码执行问题中显示的序列会发生什么:

  1. A get_bal,得0
  2. B执行get_bal,得到0
  3. A执行set_bal(0+10),帐户现在拥有10
  4. B执行set_bal(0+20),帐户现在持有20
  5. 显然这是不正确的,因为结果帐户余额应为30。

    使用正确的银行应用相同的客户序列会产生正确的金额:

    1. A执行deposit(10),帐户现在拥有10
    2. B执行deposit(20),帐户现在持有30

答案 2 :(得分:1)

正如@Stratus所说,你编写第二种方法的方式保证了存款方法不存在竞争条件的风险,因为账户流程本身使得操作在单个交易中获得平衡+更新余额。

如果你想说服自己并比较两种方法,你可以产生许多并行更新同一帐户的流程,并在完成所有存款到预期存款后比较实际余额。以下代码执行存款测试:

-module(bank).
-export([account/1, start/0, stop/0, deposit1/1, deposit2/1, get_bal/0, set_bal/1, withdraw/1]).

%test

-export ([test/3,user/3]).

account(Balance) ->
receive
    {set, NewBalance} ->
        account(NewBalance);
    {get, From} ->
        From ! {balance, Balance},
        account(Balance);
    {deposit, Amount, From} ->
        NewBalance = Balance + Amount,
        From ! {deposit, Amount, NewBalance},
        account(NewBalance);
    {withdraw, Amount, From} when Amount > Balance ->
        From ! {error, {insufficient_funds, Amount, Balance}},
        account(Balance);
    {withdraw, Amount, From} ->
        NewBalance = Balance - Amount,
        From ! {withdrawal, Amount, NewBalance},
        account(NewBalance);    
    stop -> ok
end.





start() ->
    Account_PID = spawn(bank, account, [0]),
    register(account_process, Account_PID).

stop() ->
    account_process ! stop,
    unregister(account_process).

set_bal(B) ->
    account_process ! {set, B}.

get_bal() ->
    account_process ! {get, self()},
    receive
        {balance, B} -> B
    end.

deposit1(Amount) ->
    OldBalance = get_bal(),
    NewBalance = OldBalance + Amount,
    set_bal(NewBalance).

deposit2(Amount) when Amount > 0 ->
    account_process ! {deposit, Amount, self()},
    receive
        {deposit, Amount, NewBalance} ->
            {ok, NewBalance}
    end.

withdraw(Amount) when Amount > 0 ->
    account_process ! {withdraw, Amount, self()},
    receive
        {withdrawal, Amount, NewBalance} ->
            {ok, NewBalance};
        Error ->
            Error
    end.


test(Nbuser, Nbdeposit, Method) ->
    start(),
    done = spawn_users(Nbuser,Nbdeposit,Method,self()),
    receive_loop(Nbuser),
    Res = (get_bal() == Nbdeposit*Nbuser),
    stop(),
    Res.

spawn_users(0,_Nbdeposit,_Method,_Pid) -> done;
spawn_users(Nbuser,Nbdeposit,Method,Pid) ->
    spawn(?MODULE,user,[Nbdeposit,Method,Pid]),
    spawn_users(Nbuser-1,Nbdeposit,Method,Pid).

receive_loop(0) -> done;
receive_loop(N) ->
    receive
        end_deposit -> receive_loop(N-1)
    end.

user(0,_,Pid) ->
    get_bal(), % to be sure that with method deposit1, the last set_bal is processed
    Pid ! end_deposit;
user(N,Method,Pid) ->
    ?MODULE:Method(1),
    user(N-1,Method,Pid).

你可以验证,2个用户进行1次存款,你会得到方法1的错误,而使用方法2,你甚至没有1000个用户进行1000次存款。

2> bank:test(1,100,deposit1).
true
3> bank:test(2,1,deposit1).  
false
4> bank:test(1,100,deposit2).
true
5> bank:test(2,1,deposit2).  
true
6> bank:test(1000,1000,deposit2).
true

<强>备注

结果取决于您正在使用的机器。我使用带有smp的四核,所以错误的方法立即失败,我猜它可能需要更多用户或存放在单个核心上。

答案 3 :(得分:0)

答案类似于“如何改善Erlang中的瓶颈性能”。在瓶颈的情况下,目标不是改善它(使其更具性能),而是完全消除它(这很少是不可能的)。

在排除的情况下,目标不是要证明你“在程序Y的持续时间内锁定数据X并回滚到Z”,而是以完全不需要锁的方式编写程序。 。我确信有些情况下这可能是不可避免的,但我从未在Erlang中遇到过一个(至少不是我记得的)。进程不共享内存。这就是为什么史蒂夫·维诺斯基对你之前(几乎相同)的问题(Applying a mutex into an erlang example)的回答,说明了如何组合运算而不是在外部流程API中分离它们的步骤。

如果您公开的程序add(Account, Value)完全正确loop(Current + Value)而没有其他任何事情发生,请确保您遇到麻烦。但这暴露了极低级别的API,不是吗?解决此问题的正确方法是执行Vinoski推荐的操作,并仅公开更高级别的API,该API结合了值变更的操作,报告了变更的影响。 没有机会来自另一个进程的同一表单的另一个待处理操作可以在您尝试立即读取时更改该值,从而导致一个或另一个API调用被绊倒,因为API调用是排队的消息,而不是在不同线程之间发生的C风格函数调用,它们可以以任意顺序改变内存中相同位置的基础值而不锁定它们。

进程邮箱您的互斥锁。如果你按照预期的方式使用Erlang进程,那么这类bug就不存在了。你不能搞砸了。每个操作都是完全原子的,按照消息接收的顺序排队,完全阻止/锁定数据,并且无论如何无法从外部访问基础数据

每个进程在其存在期间已对其所有数据进行独占锁定

相关问题