%% @doc Abuse module constant pools as a "read-only shared heap" (since erts 5.6)
Erlang 进程之间的消息发送都是通过数据拷贝实现的,只有一个例外就是同一个Erlang节点内的 refc binaries.关于Erlang二进制相关的内容可以参看[Erlang 0024]Erlang二进制数据处理 和 [Erlang 0032] Erlang Binary的内部实现 .消息向另外一个Erlang节点发送,首先会编码成Erlang外部数据格式(Erlang External Format)然后通过TCP/IP Socket 发送.接收到消息的节点进行消息解码然后派发到具体的进程.Erlang中就没有全局变量,像这位老兄遇到的问题,我们怎么办? Erlang中想要共享数据怎么办?
- 在当前进程内共享状态数据,首先应该想到的是使用gen_server, gen_server创建之初初始化Loop State,然后在后续操作行为中被使用,更新;
[Erlang 0023] 理解Erlang/OTP gen_server
- 在当前进程内共享数据可以使用进程字典Process Dictionary,数据保存在当前进程
进程字典无锁,哈希实现,内容参与GC,速度很快但是几乎所有的教材里面都不建议使用Erlang进程字典,主要是担心这样会造成全局变量,带来了副作用;实际应用中对于一些一次性读入就不再变化的数据,或者变动频率非常低的数据,会放在进程字典中.速度那么快,又符合我们的应用场景,用用又何妨?
看霸爷的测试:进程字典到底有多快 百万条级别,插入100ns, 查询40ns. 而ets的速度大概是us,差了一个数量级别。
- 跨进程共享数据,可以使用ETS表,从ETS表读取数据是通过内存拷贝实现的
在Erlang VM中ETS有自己的内存管理系统,拥有独立于进程的数据区域;换句话说ETS的操作就是通过内存拷贝完成的读写;这样要拷贝的数据大小就很重要了.
看这篇论文: A Study of Erlang ETS Table Implementations
mochiweb项目的mochiglobal模块提供了另外一种方法:它把需要的常量编译到新的模块,Erlang Code Server加载这个模块,就可以在各个进程之间共享数据了;同一节点内避免了数据的拷贝,先看一下是怎么用的吧:
Eshell V5.9 (abort with ^G) 1> mochiglobal:put(abc,'this_the_app_config_value_never_change'). ok 2> mochiglobal:get(abc). this_the_app_config_value_never_change 3> mochiglobal:put(abc,'ho_changed_fml'). ok 4> mochiglobal:get(abc). ho_changed_fml 5>
使用非常简单,我们可以把它看作Erlang节点内一个全局的Key_Value服务;上面说会把常量值编译到新的模块,这个什么意思呢?看下面的模块:
-module(abc). -export([term/0]). term() -> this_the_app_config_value_never_change.
动态编译的模块就是这样一个简单的实现,调用abc:term().返回值this_the_app_config_value_never_change.换句话说,我们调用mochiglobal:get(abc).实际上是执行了类似于abc:term()这样一个方法;我们还是在shell中看一下调用,因为动态编译的结果还略有不同:
Eshell V5.9 (abort with ^G) 1> abc:term(). this_the_app_config_value_never_change 2> abc:module_info(). [{exports,[{term,0},{module_info,0},{module_info,1}]}, {imports,[]}, {attributes,[{vsn,[242773849471402131574616398046036072850]}]}, {compile,[{options,[{outdir,"/box/mochi-mochiweb-b277802"}]}, {version,"4.8"}, {time,{2012,4,19,9,32,18}}, {source,"/box/mochi-mochiweb-b277802/abc.erl"}]}]
下面就要看看mochiglobal是怎么实现的了
-spec compile(atom(), any()) -> binary(). compile(Module, T) -> {ok, Module, Bin} = compile:forms(forms(Module, T), [verbose, report_errors]), Bin. -spec forms(atom(), any()) -> [erl_syntax:syntaxTree()]. forms(Module, T) -> [erl_syntax:revert(X) || X <- term_to_abstract(Module, term, T)]. -spec term_to_abstract(atom(), atom(), any()) -> [erl_syntax:syntaxTree()]. term_to_abstract(Module, Getter, T) -> [%% -module(Module). erl_syntax:attribute( erl_syntax:atom(module), [erl_syntax:atom(Module)]), %% -export([Getter/0]). erl_syntax:attribute( erl_syntax:atom(export), [erl_syntax:list( [erl_syntax:arity_qualifier( erl_syntax:atom(Getter), erl_syntax:integer(0))])]), %% Getter() -> T. erl_syntax:function( erl_syntax:atom(Getter), [erl_syntax:clause([], none, [erl_syntax:abstract(T)])])].
上面的代码term_to_abstract完成了句法构造,erl_syntax:revert将句法转成句法树,然后 compile:forms完成编译;term_to_abstract方法的三个参数Module是模块名,它实际上是做了一个key值加前缀的拼接list_to_atom("mochiglobal:" ++ atom_to_list(K)).也就是说实际上mochiglobal:get(abc).调用产生的
动态模块名是mochiglobal:abc,实际中我们几乎不会这样给模块命名,最大程度上避免了和已有命名模块冲突;注意在shell中我们调用的时候要加一下单引号'mochiglobal:abc'否则会有语法错误.Getter参数是硬编码了原子term,参数T就是我们对应的Value值了;了解了这些我们在Erlang Shell中操练一下:
Eshell V5.9 (abort with ^G) 1> mochiglobal:put(abc,'this_the_app_config_value_never_change'). ok 2> abc:term(). %%%是加了前缀的 并没有abc这个模块 ** exception error: undefined function abc:term/0 3> mochiglobal:abc:term(). %%%这样有语法错误 * 1: syntax error before: ':' 3> 'mochiglobal:abc':term(). %%%这样OK this_the_app_config_value_never_change 4> mochiglobal:put(abc,'new_value_now'). ok 5> 'mochiglobal:abc':term(). new_value_now 6> 'mochiglobal:abc':module_info(). %%看一下动态模块的元数据信息 [{exports,[{term,0},{module_info,0},{module_info,1}]}, {imports,[]}, {attributes,[{vsn,[241554446202275028379059762912985937376]}]}, {compile,[{options,[]}, %%注意这里并没有源代码的路径 {version,"4.8"}, {time,{2012,4,19,9,52,33}}, {source,"/box/mochi-mochiweb-b277802/ebin"}]}]
那我们重新复制又是怎么实现的呢?联系上面的实现,容易想到其实是走了一个代码热更新的过程
-spec get(atom()) -> any() | undefined. %% @equiv get(K, undefined) get(K) -> get(K, undefined). -spec get(atom(), T) -> any() | T. %% @doc Get the term for K or return Default. get(K, Default) -> get(K, Default, key_to_module(K)). get(_K, Default, Mod) -> try Mod:term() catch error:undef -> Default end. -spec put(atom(), any()) -> ok. %% @doc Store term V at K, replaces an existing term if present. put(K, V) -> put(K, V, key_to_module(K)). put(_K, V, Mod) -> Bin = compile(Mod, V), code:purge(Mod), {module, Mod} = code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin), ok.
类似的,执行delete方法实际上就是Erlang Code Server执行了模块的移除:
-spec delete(atom()) -> boolean(). %% @doc Delete term stored at K, no-op if non-existent. delete(K) -> delete(K, key_to_module(K)). delete(_K, Mod) -> code:purge(Mod), code:delete(Mod).
通过分析mochiglobal的实现,我们知道了它的实现机制,并且知道这种实现成本还是比较高的,每一次复制都是走了一次热更新的过程,所以它适用的场景是数据几乎不动的情况;mochiglobal同一节点内避免了数据的拷贝,一些我们希望避免大数据拷贝的场景可以考虑使用,换一个角度去想,一些配置型静态数据可以放在ETS中,更好的策略是用工具直接生成Erlang模块文件.
相关话题