http://www.cnblogs.com/zhengsyao/p/erlang_race_condition_in_digraph.html
雖然 Erlang 的廣告說得非常好,functional、share-nothing、消息傳遞,blah blah 的,好像用 Erlang 寫并發程序就高枕無憂了,但是由于 Erlang 信奉高度實用主義的哲學,所以 Erlang 中有各種各樣的后門,使得 Erlang 是一個不那么 pure 的 functional 語言,而且也是允許 share something 的。
比如說,ETS 就是一個大后門。ETS 本是為實現 Mnesia 數據庫而實現的一個“基于哈希表的無結構化 term 存儲”,但是 ETS 太好用了,能滿足高性能的字典需求,而且還可以全局訪問,完全擺脫 Erlang 設置的 share nothing、immutable variable 之類的 functional 束縛,所以很多程序會喜歡使用 ETS。
Erlang 的(為了避免錯誤而設置的)種種約束在這些使用了這些后門的情況下也會喪失威力。這不,我在標準庫的 digraph 模塊中就發現了一個潛在的 bug。下面先說一下使用 ETS 造成并發 bug 的模式,然后再看 digraph 中的這個潛在 bug。
Erlang 中有很多這樣的代碼:V2 = do_something(V1),如果 V1 和 V2 是同樣類型的復雜數據結構,根據 Erlang 的語義,do_something 對 V1 只是讀操作,然后生成新的字段,最后創建出新的 V2 返回,V1 應該是不動的。這樣其他進程也可以放心地訪問 V1,至少 V1 的狀態在系統中是一致的(consistent)。如果 V1 中使用了 ETS,并且 do_something 對 ETS 做操作了,那么這個函數就產生副作用了,以上的一致性可能就無法保證了,例如假設 V1 是這樣的數據結構:
-record(some_struct, {tab1 = ets:tab(), tab2 = ets:tab(), var1 = integer()}).
這個數據結構中有兩個 ETS 表,說明這個數據結構的狀態是由兩個 ETS 表的狀態決定的。雖然 Erlang 運行時能保證單個 ETS 表的并發訪問,但是多個表的一致性還需要程序自己來維護,因此這個數據結構在多進程訪問的情況下就會出現競爭條件。下面我們看看 digraph 模塊中的潛在 bug。
digraph 模塊是用來表示有向圖(directed graph)的。有向圖就是在一個圖中,要考慮邊(edge)中頂點(vertex)的順序。有向圖的數據結構中要保存頂點的集合以及邊的集合。某個頂點的鄰居(neighbour)指的是和這個頂點相連的頂點,由于有向圖是有方向的,所以一個頂點會有入鄰居(in-neighbour)和出鄰居(out-neighbour),前者指的是有邊射入當前頂點的頂點,后者指的是當前頂點有邊射出到的頂點。好了這是基本概念。下面我們看 digraph 模塊使用的數據結構:
1 -record(digraph, {vtab = notable :: ets:tab(), 2 etab = notable :: ets:tab(), 3 ntab = notable :: ets:tab(), 4 cyclic = true :: boolean()}).
digraph 記錄體中連續 3 個 ETS 表,看上去就有不好的事情可能發生。vtab 是頂點的表,etab 是邊的表,ntab 維護了鄰居的關系。digraph 模塊對 digraph 數據結構并發訪問做了控制,允許設置兩種模式:protected 和 private。前者只允許 digraph 的所有者進程修改,但是其他進程都可以訪問;后者只允許所有者進程修改和訪問,其他進程不能訪問(其實就是設置了 ETS 表的訪問權限)。
下面創建一個簡單的有向圖,創建幾個節點和邊:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 |
5 > DG = digraph: new (). {digraph, 28692 , 32789 , 36886 , true } 6 > digraph:add_vertex(DG, v1). v1 省略... 11 > digraph:add_edge(DG, v1, v2). [ '$e' | 0 ] 省略... 16 > digraph:in_neighbours(DG, v1). [] 17 > digraph:in_neighbours(DG, v2). [v1] 省略... 21 > digraph:out_neighbours(DG, v1). [v5,v4,v3,v2] |
命令 5> 創建了新的 digraph,輸出的 DG 值中包含了幾個 ETS id,然后添加頂點和邊。v1 頂點的入鄰居沒有,出鄰居包括 v2、v3、v4 和 v5。下面展示了這幾個 ETS 表中的項:
vertices 表中是頂點,edges 表中是邊,neighbour 表中保存了 in 和 out 的關系。其實 neighbour 表中的內容可以從 edges 表中推導出來,但是這個表在這里可以做一個緩存作用,迅速查出一個節點的鄰居關系,而不用掃描 edges 表。
從這里我們應該可以嗅到 bug 的味道了:edges 表和 neighbour 表有映射關系,如果修改一個表,那么另一個表也需要修改,否則就會出現數據不一致(inconsistent)的情況。我們來看插入邊的代碼:
1 add_edge(G, V1, V2) -> 2 do_add_edge({new_edge_id(G), V1, V2, []}, G). 3 4 do_add_edge({E, V1, V2, Label}, G) -> 5 case ets:member(G#digraph.vtab, V1) of 6 false -> {error, {bad_vertex, V1}}; 7 true -> 8 case ets:member(G#digraph.vtab, V2) of 9 false -> {error, {bad_vertex, V2}}; 10 true -> 11 case other_edge_exists(G, E, V1, V2) of 12 true -> {error, {bad_edge, [V1, V2]}}; 13 false when G#digraph.cyclic =:= false -> 14 acyclic_add_edge(E, V1, V2, Label, G); 15 false -> 16 do_insert_edge(E, V1, V2, Label, G) 17 end 18 end 19 end. 20 21 do_insert_edge(E, V1, V2, Label, #digraph{ntab=NT, etab=ET}) -> 22 ets:insert(NT, [{{out, V1}, E}, {{in, V2}, E}]), 23 ets:insert(ET, {E, V1, V2, Label}), 24 E.
diagraph:add_edge/3 是發布的接口,do_add_edge/2 做一些頂點是否存在的判斷,檢查是否能添加邊,最后 do_insert_edge/5 真正負責把邊插入相應的 ETS 表。可以看出,第 22 行插入鄰居表,第 23 行插入邊表。
這里就可能出現問題了:根據 Erlang 調度器的規則,第 22 行執行完成之后,由于 ets:insert/2 是一個 BIF 調用,因此進程有可能會被搶占。如果此時進程被搶占,那么 digraph 就處于一個 inconsistent 狀態了。如下圖所示:
假設這個 digraph 是 protected,那么其他進程是可以訪問的。如果其他進程需要 joint 訪問這兩個表的話,有可能就會出問題。果然,訪問鄰居的接口 digraph:in_neighbours/2 就是這樣一個函數,下面來看代碼:
1 in_neighbours(G, V) -> 2 ET = G#digraph.etab, 3 NT = G#digraph.ntab, 4 collect_elems(ets:lookup(NT, {in, V}), ET, 2). 5 6 collect_elems(Keys, Table, Index) -> 7 collect_elems(Keys, Table, Index, []). 8 9 collect_elems([{_,Key}|Keys], Table, Index, Acc) -> 10 collect_elems(Keys, Table, Index, 11 [ets:lookup_element(Table, Key, Index)|Acc]); 12 collect_elems([], _, _, Acc) -> Acc.
in_neighbours/2 先提取出兩個表 ET 和 NT,分別是邊表和鄰居表,然后調用 collect_elems/3,相當于做一個 joint 查詢。第 4 行首先查詢鄰居表 ets:lookup(NT, {in, V}),也就是在鄰居表中查詢節點 V 入邊。查到邊之后,collect_elems/4 的第 11 行再從邊表中去查這條邊的頂點。那么如果出現上圖中的情況,在添加邊的時候在鄰居表中插入完成之后進程被切出,那么 edges 表中的第 4 行還不存在,而 neighbours 表中圈出的那兩行是存在的。那么 collect_elems/4 執行到 11 行的 ets:lookup_element 就會拋出異常。
以上就是在標準庫的 digraph 模塊中發生競爭條件的一個例子。對于這個具體的問題來說,把 do_insert_edge/5 中那兩行 insert 換一下位置就好了,先插入邊表,再插入鄰居表,畢竟后面的表可以由前面的表推導出來,所以在訪問的時候先訪問后面的表在訪問前面的表不會出現查不到的情況,盡管本質上看數據還是有可能不一致。所以雖然可以修掉這個 bug,但是這種風格還是有風險的,比如說有 3 個或更多的表需要一致的情況。其實 digraph 模塊這種風格:創建一個數據結構,得到一個句柄,然后大家都可以修改和訪問的風格不是非常“Erlangic”。應該是像這樣的設計:DG = digraph:new(), DG1 = digraph:add_vertex(DG, v1)... 。也就是說,每次修改之后就產生一個新的變量。當然,這樣在內部就不能用 ETS 來實現了,因為每次修改的是 ETS 表,而 DG 變量綁定的 #digraph{} 里面的表 id 字段的值又沒變,所以 DG 和 DG1 的值實際還是一樣的,只是表示的意義不同了,可怕的副作用啊。
至于這是不是 bug,可能有人會爭論了:至少 ETS 查詢會拋 badarg 啊,然后這個異常會傳播到 in_neighours/2 的調用者啊,所以在調用的時候要捕捉異常啊。可是 digraph 文檔又沒說會拋異常,看源碼的時候,也沒有語法結構注明一個函數會拋什么異常,那我怎么知道什么時候要捉異常呢?就算我養成調什么函數都捉異常的習慣,可是我捉到了異常又怎么知道是怎么回事呢?特別是這種并發相關的問題,下次復現的幾率又像中獎一樣。所以 Erlang 這門語言真是讓人又愛又恨啊。
不過不爭論語言本身了,言歸正傳,本文的結論就是:在使用 ETS 的時候要注意 ETS 是全局共享的內存,要注意數據一致性的問題。
erlang 參數傳遞時是賦值or引用?
請教一個基礎問題,erlang 參數傳遞時是賦值or引用?
找不到方法測試。
求教了。
答:
erlang 中怎么會有引用這種說法,變量都是拷貝過去的。
問:
無法實現引用,是么?
我在寫的東西要傳一個大狀態。
所以疑惑了一下。
答:
erlang 所有的變量都只能在函數中傳遞,沒有全局變量這一說。
你傳遞狀態是通過改變函數參數實現的
問:
我不是很清楚當參數很大時,進行拷貝的效率如何.
能給我解惑一下么?
答
越大效率越低
另外是原子atom ,erlang 的垃圾回收機制是不回收的,除非運行結束。
問:
試想gen_server中call(_From,Status,Args)被調用時,Status就被改變了。這是不是意味著這里的Status是被引用傳遞進去的呢
我有闡述清楚么?
答:
不會
Status
的值
不會被函數改變
函數得到的只是一個新的參數邦定
綁定
簡單的說
erlang 的參數都是拷貝方式的
廣州-手 14:54:35
對,這就是內存共享。
返源
不見得多少,你的每一個函數結束就會回收當中的內存。
不存在引用
廣州-手 14:53:52
沒有引用的參數傳遞 不知會吃掉我程序的性能
上海-PHP-返源 14:54:34
如果有引用就有副作用
就沒辦法獨自運行
廣州-手 14:54:10
不知會吃掉多少性能。
但是我的status是一條很長的記錄。
廣州-手 ) 14:55:23
稍稍有點擔憂。暫時還不能測試。
廣州-手 14:56:23
你剛說的參數 愈大效率越低,是怎么回事?
上海-PHP-返源 14:57:02
記錄是單獨存儲的
不過你傳遞的時候還是拷貝
廣州-手 14:56:47
恩。
上海-PHP-返源 14:57:40
但這里具體內部實現有很多方案
譬如你不寫,只是取,我就可以共用內存。
廣州-手 14:58:03
怎么實現,求指導。
上海-PHP-返源 14:58:56
這個跟你實現沒關系哈,我說的erlang 內部。
總之你減少參數的大小
有利提高速度
廣州-手 14:58:57
這個意思是erlang的自動優化么?
上海-PHP-返源 14:59:40