diff --git a/src/docs/erlangApp相关.md b/src/docs/erlangApp相关.md new file mode 100644 index 0000000..08e867d --- /dev/null +++ b/src/docs/erlangApp相关.md @@ -0,0 +1,88 @@ +文件命名为 + application_Name.app + +格式如下: +{application,"app名字", + [ + {description,"app描述"}, + {vsn ,"版本号"}, + {id ,Id},%%app id 同 erl -id ID + {modules,[Modules]},%%app包含的模块,systools模块使用它来生成script、tar文件 + {maxP,Num},%%进程最大值 + {maxT,Time},%%app运行时间 单位毫秒 + {registered,[mod]},%%指定app 名字模块,systools用来解决名字冲突 + {included_applictions ,[XX]},%%指定子 app,只加载,但是不启动 + {applictions,[xxxx]},%%启动自己的app前,将会首先启动此列表的app + {env,[xxxx]},%%配置app的env,可以使用application:get_env获取 + {mod,{xxx,args}},%%指定app启动模块,参数,对应自己app的application behavior + {start_phases,[{xxx,xxx}]]%%指定启动阶段一些操作,对应otp application start_phase函数 + ] +} + +必须要配置的为description,vsn,modules,registered,applications。 +Application为应用名, +descripttion为应用的简单描述 +id 产品标识 +vsn 应用版本 +modules 应用所涉及到的module +registered 注册进程 +applications 本应用启动时需要事先启动的其他应用 + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +erlang官网说明 + +{application, Application, + [{description, Description}, + {id, Id}, + {vsn, Vsn}, + {modules, Modules}, 本应用程序引入的所有模块。systools 在生成启动脚本和tar文件时使用这个列表。一个模块只能在一个应用程序中定义 + {maxP, MaxP}, 已经弃用了 + {maxT, MaxT}, 时间单位为毫秒 + {registered, Names}, 注册过程的所有名称均在此应用程序中启动。systools使用这个列表来检测不同应用程序之间的名字冲突 + {included_applications, Apps}, 此应用程序包含的所有应用程序 当此应用程序启动时,应用程序控制器会自动加载所有包含的应用程序,但不会启动。假设包含应用程序的最高管理者由本应用程序的主管启动 + {applications, Apps}, 允许启动此应用程序之前必须启动的所有应用程序。systools使用这个列表来生成正确的启动脚本。缺省为空列表,但请注意所有应用程序对(至少)Kernel和STDLIB都有依赖关系 + {env, Env}, 应用程序使用的配置参数。通过调用application:get_env / 1,2来检索配置参数的值 + {mod, Start}, 指定应用程序回调模块和启动参数 + 对于作为监督树实施的应用程序,密钥mod是必需的,否则应用程序控制器不知道如何启动它。 对于没有进程的应用程序(通常是代码库,例如STDLIB),可以省略mod + {start_phases, Phases}, + {runtime_dependencies, RTDeps}]}. 应用程序依赖的应用程序版本列表 + + Value Default + ----- ------- +Application atom() - +Description string() "" +Id string() "" +Vsn string() "" +Modules [Module] [] +MaxP int() infinity +MaxT int() infinity +Names [Name] [] +Apps [App] [] +Env [{Par,Val}] [] +Start {Module,StartArgs} [] +Phases [{Phase,PhaseArgs}] undefined +RTDeps [ApplicationVersion] [] + +Module = Name = App = Par = Phase = atom() +Val = StartArgs = PhaseArgs = term() +ApplicationVersion = string() + +如果要使用systools中的函数 需要设置下面的key参数 + description vsn modules registered applications + 其他的key被systools忽略 + +应用的策略 + application:start(Name, Type) +type: +• permanent: if the app terminates, the entire system is taken down, excluding manual termination of the app with application:stop/1. +• transient: if the app terminates for reason normal, that’s ok. Any other reason for termination shuts down the entire system. +• temporary: the application is allowed to stop for any reason. It will be reported, but nothing bad will happen. + + + + + + + + + \ No newline at end of file diff --git a/src/docs/erlang二进制.md b/src/docs/erlang二进制.md new file mode 100644 index 0000000..2dd26e6 --- /dev/null +++ b/src/docs/erlang二进制.md @@ -0,0 +1,95 @@ +## 二进制语法 + Bin = <> + <> = Bin + 每个E 1..n 指定bitstring的Segment + 每个段具有以下一般语法: + Value:Size/TypeSpecifierList + 可以省略Size或TypeSpecifier或两者。因此,允许以下变体: + Ei = + Value | + Value:Size | + Value/TypeSpecifierList | + Value:Size/TypeSpecifierList +### Value + 当在二进制构造中使用时,Value部分是任何表达式,用于求值为整数,浮点或位串,如果表达式不是单个文字或变量,则将其括在括号中。 + 在二进制匹配中使用时,用于位串匹配,Value必须是变量,或整数,浮点或字符串,简单而言就是Value部分必须是文字或变量。 +### Size + 在位串构造中使用,Size是要求求整数的表达式。 + 用于位串匹配,Size必须是整数,或绑定到整数的变量。 + Size的值以Unit指定段的大小,默认值取决于类型 + • For integer it is 8. + • For float it is 64. + • For binary and bitstring it is the whole binary or bit string. + 在匹配中,此默认值仅对最后一个元素有效。匹配中的所有其他位串或二进制元素必须具有大小规范,段的大小Size部分乘以TypeSpecifierList中的unit(稍后描述)给出了段的位数.对于utf8,utf16和utf32类型,不得给出Size的大小。段的大小由类型和值本身隐式确定。 + +### TypeSpecifierList + 是一个类型说明符列表,按任何顺序,用连字符("-")分隔。默认值用于任何省略的类型说明符 +#### Type + Type= integer | float | binary | bytes | bitstring | bits | utf8 | utf16 | utf32 + 默认值为integer。bytes是二进制的简写,bits是bitstring的简写。有关utf类型的更多信息,请参见下文。 +#### Signedness + Signedness= signed | unsigned + 只有匹配和类型为整数时才有意义。默认值为无符号。 +#### Endianness + Endianness= big | little | native + Native-endian意味着字节顺序在加载时被解析为big-endian或little-endian,具体取决于运行Erlang机器的CPU的本机内容。仅当Type为integer,utf16,utf32或float时,字节顺序才有意义。默认值为big。 +#### Unit + Unit= unit:IntegerLiteral + 允许的范围是1..256。对于integer,float和bitstring,默认值为1;对于binary,默认值为8。对于utf8,utf16和utf32类型,不能给出Unit说明符。 + 它与Size说明符相乘,以给出段的有效大小。单位大小指定没有大小的二进制段的对齐方式,二进制类型的段必须具有可被8整除的大小 + +### 注意 + 构造二进制文件时,如果整数段的大小N太小而不能包含给定的整数,则整数的最高有效位将被静默丢弃,并且只有N个最低有效位被放入二进制。 +#### 例子: + X:4/little-signed-integer-unit:8 + 该元素的总大小为4 * 8 = 32位,它包含一个小端序的有符号整数 + +### 关于 utf8 utf16 utf32 + 构造utf类型的段时,Value必须是0..16#D7FF或16#E000 .... 16#10FFFF范围内的整数。如果Value超出允许范围,则构造将失败并返回badarg异常。生成的二进制段的大小取决于类型或值,或两者: + • For utf8, Value is encoded in 1-4 bytes. + • For utf16, Value is encoded in 2 or 4 bytes. + • For utf32, Value is always be encoded in 4 bytes. + 构造时,可以给出一个文字字符串,后跟一个UTF类型,例如:<<“abc”/ utf8 >>,这是<< $ a / utf8,$ b / utf8,$ c / utf8的语法糖>>。 + 成功匹配utf类型的段,得到0..16#D7FF或16#E000..16#10FFFF范围内的整数。 + 如果返回值超出这些范围,则匹配失败。 + + +### 如何实现二进制文件 + 在内部,二进制和位串以相同的方式实现。 + 内部有四种类型的二进制对象: + 两个是二进制数据的容器,称为: + • Refc binaries (short for reference-counted binaries) + • Heap binaries + 两个仅仅是对二进制文件的一部分的引用,被称为: + • sub binaries + • match contexts + +### Refc Binaries + Refc二进制文件由两部分组成: + •存储在进程堆上的对象,称为ProcBin + •二进制对象本身,存储在所有进程堆之外 + 任何数量的进程都可以通过任意数量的ProcBins引用二进制对象。该对象包含一个引用计数器,用于跟踪引用的数量,以便在最后一个引用消失时将其删除。 + 进程中的所有ProcBin对象都是链表的一部分,因此当ProcBin消失时,垃圾收集器可以跟踪它们并减少二进制文件中的引用计数器。 + +### Heap Binaries + 堆二进制文件是小型二进制文件,最多64个字节,并直接存储在进程堆上。它们在进程被垃圾收集时以及作为消息发送时被复制。它们不需要垃圾收集器进行任何特殊处理。 +### Sub Binaries + The reference objects sub binaries and match contexts can reference part of a refc binary or heap binary + 子二进制文件由split_binary / 2创建或者当二进制文件以二进制模式匹配时。子二进制是对另一个二进制文件(refc或堆二进制文件的一部分,但从不进入另一个子二进制文件)的引用。因此,匹配二进制文件相对便宜,因为实际的二进制数据永远不会被复制。 +### Match Context + 匹配上下文类似于子二进制,但针对二进制匹配进行了优化 + +### 关于iolist + 定义(直接引用霸爷的文章) + 1. [] + 2. binary + 3. 列表, 每个元素是int(0-255)或者binary或者iolist. + 其中binary是指 bitsize % 8 == 0 . + int 是0-255 + Iolist的作用是用于往port送数据的时候.由于底层的系统调用如writev支持向量写, 就避免了无谓的iolist_to_binary这样的扁平话操作, 避免了内存拷贝,极大的提高了效率. + 另外额外补充: + erlang中列表时在头部添加比较高效,但是binary是在尾部追加更高效 + +### 关于消息接收转发解码和发送 + erlang通常会将接收到的消息由网关进程转发给其他工作进程, 建议先匹配消息id, 然后转发二进制消息到工作进程,然后由工作进程解码再处理 + 同时广播消息可先编码成二进制之后再广播, 避免重复编码 \ No newline at end of file diff --git a/src/docs/erlang套接字编程.md b/src/docs/erlang套接字编程.md new file mode 100644 index 0000000..e1b65e4 --- /dev/null +++ b/src/docs/erlang套接字编程.md @@ -0,0 +1,448 @@ +## gen_tcp 编程接口 + +#### listen(Port, Options) -> {ok, ListenSocket} | {error, Reason} + Types + Port = inet:port_number() + Options = [listen_option()] + ListenSocket = socket() + Reason = system_limit | inet:posix() + 设置一个套接字以侦听本地主机上的端口Port。 + + 用法: + listen(Port, Options) -> {ok, ListenSocket} | {error, Reason} + 在本地开启一个监听某个端口的套接字(socket)。开启成功的话,会返回一个套接字标识符 Socket,其一般会传递给 get_tcp:accept/1 或 get_tcp:accept/2 调用。 + + 如果参数 Port 为 0,那么底层操作系统将赋值一个可用的端口号,可以使用 inet:port/1 来获取一个 socket 监听的端口。 + + 连接到IP地址为Address的主机的TCP端口Port上的服务器。参数 地址可以是主机名或IP地址。 + 提供以下选项: + {ip, Address} + 如果主机有许多网络接口,则此选项指定要使用的接口。 + {ifaddr, Address} + 与{ip,Address}相同。如果主机有许多网络接口,则此选项指定要使用的接口。 + {fd, integer() >= 0} + 如果以某种方式未使用gen_tcp连接了套接字 ,请使用此选项传递文件描述符。如果将{ip,Address}和/或 {port,port_number()}与该选项结合使用,则 在连接前将fd绑定到指定的接口和端口。如果未指定这些选项,则假定fd已被适当绑定。 + inet + 为IPv4设置套接字。 + inet6 + 设置用于IPv6的套接字。 + local + 设置Unix域套接字。见 inet:local_address() + {port,Port} + 指定要使用的本地端口号。 + + {tcp_module, module()} + 覆盖使用哪个回调模块。默认为 inet_tcp IPv4和inet6_tcp使用IPv6。 + Opt + 参见 inet:setopts / 2。 + + 可以使用send / 2将数据包发送到返回的套接字Socket。 从对等方发送的数据包将作为消息传递: + {tcp, Socket, Data} + + 如果套接字处于{active,N}模式(有关详细信息,请参见inet:setopts / 2),并且其消息计数器降至0,则将传递以下消息以指示套接字已转换为被动({active,false}) 模式: + {tcp_passive, Socket} + + 如果套接字已关闭,则会发出以下消息: + {tcp_closed, Socket} + + 如果套接字上发生错误,则会传递以下消息(除非在套接字的选项列表中指定了{active,false},在这种情况下,可通过调用recv / 2来检索数据包): + {tcp_error, Socket, Reason} + + 可选的Timeout参数指定超时(以毫秒为单位)。默认为infinity。 + 注意::: + 请记住,如果底层OS connect()的调用返回超时,调用gen_tcp:连接也将返回超时(即{错误,ETIMEDOUT} ),即使较大的超时指定。 + 指定要连接的选项的默认值会受到内核配置参数 inet_default_connect_options的影响。有关详细信息,请参见 inet(3)。 + + 参数 Options 的一些常用选项: + + {active, true}:套接字设置为主动模式。所有套接字接收到的消息都作为 Erlang 消息转发到拥有这个套接字进程上。当开启一个套接字时,默认是主动模式。 + {active, false}:设置套接字为被动模式。套接字收到的消息被缓存起来,进程必须通过调用函数 gen_tcp:recv/2 或 gen_tcp:recv/3 来读取这些消息。 + {active, once}:将设置套接字为主动模式,但是一旦收到第一条消息,就将其设置为被动模式,并使用 gen_tcp:recv/2 或 gen_tcp:recv/3 函数来读取后续消息。 + {keepalive, true}:当没有转移数据时,确保所连接的套接字发送保持活跃(keepalive)的消息。因为关闭套接字消息可能会丢失,如果没有接收到保持活跃消息的响应,那么该选项可确保这个套接字能被关闭。默认情况下,该标签是关闭的。 + {nodelay, true}:数据包直接发送到套接字,不过它多么小。在默认情况下,此选项处于关闭状态,并且与之相反,数据被聚集而以更大的数据块进行发送。 + {packet_size, Size}:设置数据包允许的最大长度。如果数据包比 Size 还大,那么将认为这个数据包无效。 + {packet, 0}:表示 Erlang 系统会把 TCP 数据原封不动地直接传送给应用程序 + {reuseaddr, true}:允许本地重复使用端口号 + {delay_send, true}:数据不是立即发送,而是存到发送队列里,等 socket 可写的时候再发送 + {backlog, 1024}:缓冲区的长度 + {exit_on_close, false}:设置为 flase,那么 socket 被关闭之后还能将缓冲区中的数据发送出去 + {send_timeout, 15000}:设置一个时间去等待操作系统发送数据,如果底层在这个时间段后还没发出数据,那么就会返回 {error,timeout} + + {Rand, _RandSeed} = random:uniform_s(9999, erlang:now()), + Port = 40000 + Rand, + gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]). + +#### accept(ListenSocket) -> {ok, Socket} | {error, Reason} accept(ListenSocket, Timeout) -> {ok, Socket} | {error, Reason} + Types + ListenSocket = socket() Returned by listen/2. + Timeout = timeout() Socket = socket() Reason = closed | timeout | system_limit | inet:posix() + 在侦听套接字上接受传入的连接请求。 套接字必须是从listen / 2返回的套接字 。 超时以毫秒为单位指定超时值。默认为infinity。 + + 返回值: + {ok, Socket} if a connection is established + {error, closed} if ListenSocket is closed + {error, timeout} if no connection is established within the specified time + {error, system_limit} if all available ports in the Erlang emulator are in use + A POSIX error value if something else goes wrong, see inet(3) for possible error values + + 用法: + 该函数会引起进程阻塞,直到有一个连接请求发送到监听的套接字。 + + 如果连接已建立,则返回 {ok,Socket}; + 或如果 ListenSocket 已经关闭,则返回{error,closed}; + 或如果在指定的时间内连接没有建立,则返回{error,timeout}; + 或如果 Erlang 虚拟机里可用的端口都被使用了,则返回 {error, system_limit}; + 如果某些东西出错,也可能返回一个 POSIX 错误。一些有可能的错误请查看 inet 模块的相关说明。 + + 使用 gen_tcp:send/2 向该函数返回的套接字 Socket 发送数据包。往端口发送的数据包会以下面格式的消息发送: + {tcp, Socket, Data} + 如果在建立套接字 Socket 的时候选项列表中指定了 {active,false},这样就只能使用 gen_tcp:recv/2 或 gen_tcp:recv/3 来接收数据包了。 + + {Rand, _RandSeed} = random:uniform_s(9999, erlang:now()), + Port = 40000 + Rand, + case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of + {ok, ListenSocket} -> + case gen_tcp:accept(ListenSocket) of + {ok, Socket} -> + Socket; + {error, SocketAcceptFail} -> + SocketAcceptFail + end; + _ -> + socket_listen_fail + end. + +#### connect(Address, Port, Options) -> {ok, Socket} | {error, Reason} +#### connect(Address, Port, Options, Timeout) -> {ok, Socket} | {error, Reason} + Types + Address = inet:socket_address() | + inet:hostname() Port = inet:port_number() + Options = [connect_option()] + Timeout = timeout() + Socket = socket() + Reason = timeout | inet:posix() + 连接一个 TCP 端口 + + 用法: + connect(Address, Port, Options, Timeout) -> {ok, Socket} | {error, Reason} + 用给出的端口 Port 和 IP 地址 Address 连接到一个服务器上的 TCP 端口上。参数 Address 即可以是一个主机名,也可以是一个 IP 地址。 + 提供以下选项: + {ip, Address} + 如果主机有许多网络接口,则此选项指定要使用的接口。 + {ifaddr, Address} + 与{ip,Address}相同。如果主机有许多网络接口,则此选项指定要使用的接口。 + {fd, integer() >= 0} + 如果以某种方式未使用gen_tcp连接了套接字 ,请使用此选项传递文件描述符。如果将{ip,Address}和/或 {port,port_number()}与该选项结合使用,则 在连接前将fd绑定到指定的接口和端口。如果未指定这些选项,则假定fd已被适当绑定。 + inet + 为IPv4设置套接字。 + inet6 + 设置用于IPv6的套接字。 + local + 设置Unix域套接字。见 inet:local_address() + {port,Port} + 指定要使用的本地端口号。 + + {tcp_module, module()} + 覆盖使用哪个回调模块。默认为 inet_tcp IPv4和inet6_tcp使用IPv6。 + Opt + 参见 inet:setopts / 2。 + + 可以使用send / 2将数据包发送到返回的套接字Socket。 从对等方发送的数据包将作为消息传递: + {tcp, Socket, Data} + + 如果套接字处于{active,N}模式(有关详细信息,请参见inet:setopts / 2),并且其消息计数器降至0,则将传递以下消息以指示套接字已转换为被动({active,false}) 模式: + {tcp_passive, Socket} + + 如果套接字已关闭,则会发出以下消息: + {tcp_closed, Socket} + + 如果套接字上发生错误,则会传递以下消息(除非在套接字的选项列表中指定了{active,false},在这种情况下,可通过调用recv / 2来检索数据包): + {tcp_error, Socket, Reason} + + 可选的Timeout参数指定超时(以毫秒为单位)。默认为infinity。 + 注意::: + 请记住,如果底层OS connect()的调用返回超时,调用gen_tcp:连接也将返回超时(即{错误,ETIMEDOUT} ),即使较大的超时指定。 + 指定要连接的选项的默认值会受到内核配置参数 inet_default_connect_options的影响。有关详细信息,请参见 inet(3)。 + +#### gen_tcp:close/1 + Types + Socket = socket() + 关闭一个 TCP 套接字 + 请注意,在大多数TCP实现中,执行关闭操作并不能保证在远程端检测到关闭之前,已发送的任何数据都会传递给接收方。如果要保证将数据传递给收件人,可以通过两种常用方法来实现。 + 使用gen_tcp:shutdown(Sock,write)发出信号,表明不再发送任何数据,并等待套接字的读取端关闭。 + 使用套接字选项{packet,N}(或类似的选项)可以使接收器在知道已接收到所有数据时关闭连接。 + + +#### recv(Socket, Length) -> {ok, Packet} | {error, Reason} +#### recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason} + Types + Socket = socket() + Length = integer() >= 0 + Timeout = timeout() + Packet = string() | binary() | HttpPacket + Reason = closed | timeout | inet:posix() + HttpPacket = term() 看到的描述 HttpPacket中 的erlang:decode_packet / 3 在ERTS。 + + 在被动模式下从套接字接收数据包。 + 返回值{error,closed}指示关闭的套接字。 + Argument Length is only meaningful when the socket is in raw mode and denotes the number of bytes to read. + 参数 Length 仅在套接字处于 raw mode 时才有意义,它表示要读取的字节数。 + + Length为0,则返回所有可用字节。 + 如果Length > 0,则返回确切的 Length字节,否则返回错误; + 从另一侧关闭套接字时,可能会丢弃少于长度字节的数据 + 可选的Timeout参数指定超时(以毫秒为单位)。默认为infinity。 + + 用法: + recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason} + 这个函数是从一个被动模式的套接字接受一个数据包。如果返回一个 {error, closed} 的返回值,那表明 Socket 已经关闭。 + + 当 Socket 是 raw 模式下,参数 Length 才有意义的,并且 Length 表示接收字节的大小。如果 Length = 0,所有有效的字节数据都会被接收。如果 Length > 0,则只会接收 Length 长度的字节,或发生错误;当另一端 Socket 关闭时,接收的数据长度可能会小于 Length。 + + 选项 Timeout 是一个以毫秒为单位的超时值,默认值是 infinity。 + + {Rand, _RandSeed} = random:uniform_s(9999, erlang:now()), + Port = 40000 + Rand, + case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of + {ok, ListenSocket} -> + case gen_tcp:accept(ListenSocket) of + {ok, Socket} -> + gen_tcp:recv(Socket, 0, 5000); + {error, SocketAcceptFail} -> + SocketAcceptFail + end; + _ -> + socket_listen_fail + end. + +#### send(Socket, Packet) -> ok | {error, Reason} + Types + Socket = socket() + Packet = iodata() + Reason = closed | inet:posix() + 在一个套接字 Socket 发送一个数据包 + 用法: + send(Socket, Packet) -> ok | {error, Reason} + 在一个套接字 Socket 发送一个数据包。 + +#### shutdown(Socket, How) -> ok | {error, Reason} + Types + Socket = socket() + How = read | write | read_write + Reason = inet:posix() + 在一个或两个方向上关闭socket + + 以某种方式半关闭一个套接字。 + + 如果参数 How 为 write 的形式,则套接字 socket 会关闭数据写入,读取仍可以正常执行。 + 如果How == read或Socket端口没有缓冲传出数据,则套接字将立即关闭,并且Reason中将返回遇到的任何错误。 + 要实现套接字半打开, 那么套接字要设置 {exit_on_close, false} 这个参数。 + 如果套接字端口中缓冲了数据,则将尝试关闭套接字的操作推迟到该数据写入内核套接字发送缓冲区中为止。 + 如果遇到任何错误,则关闭套接字,并在下一个recv / 2或 send / 2上返回 {error,closed}。 + + 如果对等方在写端执行了关闭操作,则选项{exit_on_close,false}很有用。 + + +#### gen_tcp:controlling_process/2 + 改变一个套接字的控制进程 + + 将新的控制过程Pid分配给 Socket。控制过程是从套接字接收消息的过程。 + 如果由当前控制进程以外的任何其他进程调用, 则返回{error,not_owner}。 + 如果由Pid标识的进程不是现有的本地pid, 则返回{error,badarg}。 + 在某些情况下,在执行此函数期间关闭Socket时,也可能返回{error,badarg}。 + + 如果套接字设置为活动模式,则此功能会将呼叫者邮箱中的所有消息传送到新的控制进程。 + 如果在传输过程中有任何其他进程正在与套接字交互,则传输可能无法正常进行,并且消息可能会保留在呼叫者的邮箱中。 + 例如,在传输完成之前更改套接字活动模式可能会导致此情况 + +#### 套接字选项 + {active, true | false | once | -32768..32767} | + 如果值为true,这是默认值,则将从套接字接收的所有内容作为消息发送到接收进程。 + 如果值为false(被动模式),则该进程必须通过调用gen_tcp:recv / 2,3, gen_udp:recv / 2,3或gen_sctp:recv / 1,2来显式接收传入的数据 (取决于套接字的类型) )。 + 如果该值为一次({active,once}), 则套接字中的一条数据消息将发送到该进程。要接收更多消息, 必须使用选项{active,一次}再次调用 setopts / 2。 + + 如果该值是-32768到32767(含)之间的整数N,则将该值添加到发送到控制进程的套接字的数据消息计数中。 + 套接字的默认消息计数为0。如果指定了负值,并且其大小等于或大于套接字的当前消息计数,则套接字的消息计数将设置为0。 + 一旦套接字的消息计数达到0,则可能是由于 向进程发送接收到的数据消息或通过显式设置该消息, + 然后通过特定于套接字类型的特殊消息通知该进程套接字已进入被动模式。 一旦套接字进入被动模式,为了接收更多消息, + 必须再次调用setopts / 2才能将套接字设置回主动模式。 + 如果该值是-32768到32767(含)之间的整数N,则将该值添加到发送到控制进程的套接字的数据消息计数中。套接字的默认消息计数为0。 + 如果指定了负值,并且其大小等于或大于套接字的当前消息计数,则套接字的消息计数将设置为0。一旦套接字的消息计数达到0, + 要么是由于向进程发送接收到的数据消息,要么是因为已显式设置它,然后通过特定于套接字类型的特殊消息通知该进程该套接字已进入被动模式。 + 一旦套接字进入被动模式,为了接收更多消息,必须再次调用setopts / 2才能将套接字设置回主动模式。 + 使用{active,一次}或{active,N}时,套接字在接收到数据时会自动更改行为。与面向连接的套接字(即gen_tcp)结合使用时, + 可能会造成混淆,因为具有{active,false}行为的套接字报告的关闭方式与具有{active,true} 行为的套接字关闭的方式不同。为了简化编程, + 当套接字在{active,false}模式下被关闭且对等方关闭时, 在设置为{active,一旦}时仍会生成消息 {tcp_closed,Socket }, + {active,true}或{active,N}模式。因此可以肯定地假设,当套接字在{active,true}和 {active,false}模式之间来回切换时, + 消息 {tcp_closed,Socket}可能最终会出现套接字端口终止(取决于选项exit_on_close)。 + 但是, 当检测到对等关闭时,完全取决于基础的TCP / IP堆栈和协议。 + 注意{active,true}模式不提供流量控制;快速的发送者可以轻松地使接收者的传入消息溢出。对于 {active,N}模式,消息数大于零时也是如此。 + 仅当高级协议提供自己的流控制(例如,确认收到的消息)或交换的数据量很少时,才使用活动模式。{active,false} 模式, + 使用{active,一旦}模式或{active,N} 模式(具有适用于应用程序的N值)提供流量控制。另一端发送的速度不能超过接收器可以读取的速度。 + + {broadcast, Boolean} (UDP sockets) + 启用/禁用发送广播的权限。 + {buffer, integer() >= 0} | + 驱动程序使用的用户级缓冲区的大小。不要与sndbuf 和recbuf选项混淆,它们与内核套接字缓冲区相对应。对于TCP,建议使用val(buffer)> = val(recbuf), + 以避免由于不必要的复制而导致的性能问题。对于UDP,适用相同的建议,但最大值不应大于网络路径的MTU。 + 设置recbuf时,val(buffer)会自动设置为上述最大值。但是,为Recbuf设置的大小 通常变大,建议您使用 getopts / 2 来分析操作系统的行为。 + 请注意,这也是从单个recv调用可以接收的最大数据量。如果您使用的MTU高于正常值,请考虑将缓冲区设置为更高。 + {delay_send, boolean()} | + 通常,当Erlang进程发送到套接字时,驱动程序会尝试立即发送数据。如果失败,驱动程序将使用任何可用方法将要发送的消息排队, + 只要操作系统表示可以处理该消息。设置{delay_send,true} 会使所有消息排队。这样,发送到网络的消息就更大, + 但更少。该选项将影响发送请求与Erlang进程的调度,而不是更改套接字的任何实际属性。该选项是特定于实现的。默认为false。 + {deliver, port | term} | + 当{active,true}时,数据在以下端口上传递 {S, {data, [H1,..Hsz | Data]}} or term : {tcp, S, [H1..Hsz | Data]}. + {dontroute, boolean()} | + 启用/禁用传出消息的路由旁路 + {exit_on_close, boolean()} | + 默认情况下,此选项设置为true。 + 将其设置为false的唯一原因是,如果要在检测到关闭后继续向套接字发送数据,例如,如果对等方使用 gen_tcp:shutdown / 2 关闭写端。 + {header, integer() >= 0} | + 仅当创建套接字时指定了选项binary 时,此选项才有意义。如果指定了选项 header, + 则从套接字接收的数据的第一个 Size Number字节是列表的元素,其余数据是指定为同一列表尾部的二进制文件。例如,如果Size == 2,则接收到的数据与[Byte1,Byte2 | Binary]匹配 + {high_msgq_watermark, integer() >= 1} | + 当消息队列上的数据量达到此限制时,套接字消息队列将设置为繁忙状态。请注意,此限制仅涉及尚未达到ERTS内部套接字实现的数据。默认为8 kB。 + 如果套接字消息队列繁忙或套接字本身繁忙,则挂起套接字的数据发送器。 + 有关更多信息,请参见选项low_msgq_watermark, high_watermark和low_watermark。 + Notice that distribution sockets disable the use of high_msgq_watermark and low_msgq_watermark. Instead use the distribution buffer busy limit, which is a similar feature. + {high_watermark, integer() >= 0} | + 当ERTS套接字实现在内部排队的数据量达到此限制时,将套接字设置为繁忙状态。默认为8 kB。 + 如果套接字消息队列繁忙或套接字本身繁忙,则挂起套接字的数据发送器。 + 有关更多信息,请参见选项low_watermark, high_msgq_watermark和low_msqg_watermark。 + {ipv6_v6only, Boolean} + 限制套接字仅使用IPv6,禁止任何IPv4连接。这仅适用于IPv6套接字(选项inet6)。 + 在大多数平台上,必须先在套接字上设置此选项,然后才能将其与地址关联。因此,仅在创建套接字时指定它,而在调用包含此描述的函数(setopts / 2)时不使用它是合理的。 + 将此选项设置为true的套接字的行为 是唯一可移植的行为。现在,FreeBSD不建议使用IPv6的初衷是将IPv6用于所有流量(您可以使用 {ipv6_v6only,false}来覆盖建议的系统默认值),但OpenBSD(受支持的GENERIC内核)禁止使用,并且在Windows(具有单独的IPv4和IPv6协议栈)。大多数Linux发行版的系统默认值仍为false。逐渐改变了操作系统之间从IPv4流量中分离IPv6流量的策略,因为逐渐证明,要确保正确,安全地实现双堆栈实施是困难而复杂的。 + 在某些平台上,此选项唯一允许的值为true,例如OpenBSD和Windows。在这种情况下,尝试在创建套接字时将此选项设置为false会失败。 + 在不存在的平台上设置此选项将被忽略。使用getopts / 2获取此选项 不会返回任何值,即返回的列表不包含 {ipv6_v6only,_}元组。在Windows上,该选项不存在,但会将其模拟为值为true的只读选项。 + 因此, 在创建套接字时将此选项设置为true永远不会失败,除非可能是在您已将内核自定义为仅允许false的平台上进行,但在OpenBSD上这是可行的(但尴尬)。 + 如果使用getopts / 2读回选项值 而没有获取任何值,则该选项在主机操作系统中不存在。IPv6和IPv4套接字在同一端口上侦听的行为以及获取IPv4流量的IPv6套接字的行为不再可预测。 + {keepalive, boolean()} | + 没有其他数据交换时,启用/禁用连接的套接字上的定期传输。如果另一端没有响应,则认为连接已断开,并且将错误消息发送到控制过程。默认为禁用。 + {linger, {boolean(), integer() >= 0}} | + 确定在close / 1套接字调用中刷新未发送数据的超时(以秒为单位)。 + 第一个组件是如果启用了延迟,第二个组件是刷新超时(以秒为单位)。有3种选择: + {false,_} + close / 1或shutdown / 2会立即返回,而不是等待刷新数据,而在后台进行关闭。 + {true,0} + 关闭连接时中止连接。丢弃仍保留在发送缓冲区中的所有数据,并将RST发送给对等方。 + 这避免了TCP的TIME_WAIT状态,但是使创建该连接的另一个“化身”成为可能。 + 当时间> 0时,{true,时间} + 在成功发送了套接字的所有排队消息或达到了超时(时间)之前,close / 1或shutdown / 2不会返回。 + {low_msgq_watermark, integer() >= 1} | + 如果套接字消息队列处于繁忙状态,则当消息队列中排队的数据量低于此限制时,套接字消息队列将设置为不繁忙状态。请注意,此限制仅涉及尚未达到ERTS内部套接字实现的数据。默认为4 kB。 + 当套接字消息队列和套接字不繁忙时,将恢复由于繁忙的消息队列或繁忙的套接字而挂起的发件人。 + 有关更多信息,请参见选项high_msgq_watermark, high_watermark和low_watermark。 + 请注意,分发套接字禁止使用 high_msgq_watermark和low_msgq_watermark。而是使用 分配缓冲区繁忙限制,这是一个类似功能。 + {low_watermark, integer() >= 0} | + 如果套接字处于繁忙状态,则当ERTS套接字实现在内部排队的数据量低于此限制时,会将套接字设置为不繁忙状态。默认为4 kB。 + 当套接字消息队列和套接字不繁忙时,将恢复由于繁忙的消息队列或繁忙的套接字而挂起的发件人。 + 有关更多信息,请参见选项high_watermark, high_msgq_watermark和low_msgq_watermark + {mode, list | binary} | + 接收到的数据包按照list或者binary的定义进行传递。 + list | + 接收到的数据包以列表形式发送。 + binary | + 接收到的数据包以二进制形式传送 + {bind_to_device,Ifname :: binary()} + 将套接字绑定到特定的网络接口。必须在创建套接字的函数调用中使用此选项,即 gen_tcp:connect / 3,4, gen_tcp:listen / 2, gen_udp:open / 1,2或 gen_sctp:open / 0,1,2。 + 与getifaddrs / 0不同,Ifname编码为二进制。如果系统在网络设备名称中使用非7位ASCII字符(这种情况不太可能发生),则在对该参数进行编码时必须格外小心。 + 此选项使用特定于Linux的套接字选项 SO_BINDTODEVICE,例如在Linux内核2.0.30或更高版本中,因此仅在针对此类操作系统编译运行时系统时才存在。 + 在Linux 3.8之前,可以设置此套接字选项,但无法使用getopts / 2进行检索。从Linux 3.8开始,它是可读的。 + 虚拟机还需要提升的特权,这些特权可以以超级用户身份运行,或者(对于Linux)具有CAP_NET_RAW能力 。 + 此选项的主要用例是将套接字绑定到 Linux VRF实例。 + {nodelay, boolean()} | + {nodelay,布尔值}(TCP / IP套接字) + 如果Boolean == true, 则为套接字打开选项TCP_NODELAY,这意味着也会立即发送少量数据。 + {nopush,布尔型}(TCP / IP套接字) + 这相当于TCP_NOPUSH在BSD和TCP_CORK在Linux上。 + 如果Boolean == true,则为套接字打开相应的选项,这意味着将累积少量数据,直到可用完整的MSS数据为止或关闭此选项。 + 请注意,虽然OSX上提供了TCP_NOPUSH套接字选项,但其语义却大不相同(例如,取消设置它不会导致立即发送累积的数据)。因此,在OSX上有意忽略了nopush选项 + {packet, 0 | 1 | 2 | 4 | raw | sunrm | asn1 | cdr | fcgi | line | tpkt | http | httph | http_bin | httph_bin} | + raw | 0 没有包装。 + 1 | 2 | 4 数据包包含一个标头,该标头指定了数据包中的字节数,然后是该字节数。标头长度可以是一个, + 两个或四个字节,并包含一个按big-endian字节顺序排列的无符号整数。每个发送操作都会生成标头,并且在每个接收操作上都会剥离标头。4字节的标头限制为2Gb。 + asn1 | cdr | sunrm | fcgi | tpkt | line + 这些数据包类型仅对接收有效。发送数据包时,应用程序有责任提供正确的标头。但是,在接收时,对于接收到的每个完整数据包,将一条消息发送到控制过程,并且类似地,对gen_tcp:recv / 2,3的每次调用都 返回一个完整数据包。标头未剥离。 + 数据包类型的含义如下: + asn1 -ASN.1 BER + sunrm -Sun的RPC编码 + CDR -CORBA(GIOP 1.1) + fcgi-快速CGI + tpkt -TPKT格式[RFC1006] + line-行模式,数据包以换行符结尾的行,比接收缓冲区长的行被截断 + http | http_bin + 超文本传输​​协议。按照ERTS的erlang:decode_packet / 3中 描述的 HttpPacket格式返回数据包。被动模式下的套接字从gen_tcp:recv返回{ok,HttpPacket}, 而主动套接字发送诸如 {http,Socket,HttpPacket}之类的消息。 + httph | httph_bin + 通常不需要这两种类型,因为在读取第一行之后,套接字会在内部自动从http / http_bin切换到 httph / httph_bin。但是,有时可能有用,例如从分块编码中解析预告片 + {packet_size, integer() >= 0} | + 设置数据包主体的最大允许长度。如果数据包头指示数据包的长度大于最大允许长度,则该数据包被视为无效。如果数据包头对于套接字接收缓冲区太大,则会发生相同的情况。 + 对于面向行的协议(line,http *),选项packet_size还可以保证接受指定长度的行,并且由于内部缓冲区的限制,该行不会被视为无效。 + {line_delimiter, Char}(TCP/IP sockets) + 设置面向行的协议(line)的行定界字符。默认为$ \ n。 + {priority, integer() >= 0} | + 在实现此功能的平台上设置SO_PRIORITY套接字级别选项。行为和允许范围在不同系统之间有所不同。该选项在未实现的平台上被忽略。请谨慎使用。 + {raw,Protocol :: integer() >= 0,OptionNum :: integer() >= 0, ValueBin :: binary()} | + {read_packets,Integer}(UDP套接字) + 设置在数据可用时无需套接字就可以读取的最大UDP数据包数。当读取了这么多的数据包并将其传送到目标进程后,新的数据包才被读取,直到有可用数据的新通知到达为止。默认为5。如果此参数设置得太高,由于UDP数据包泛洪,系统可能会变得无响应。 + + {recbuf, integer() >= 0} | + 用于套接字的接收缓冲区的最小大小。建议您使用 getopts / 2 来检索操作系统设置的大小。 + {reuseaddr, boolean()} | + 允许或禁止端口号的本地重用。默认情况下,不允许重用。 + {send_timeout, integer() >= 0 | infinity} | + 仅允许用于面向连接的套接字。 + 指定最长时间等待基础TCP堆栈接受发送操作。当超过限制时,发送操作将返回 {error,timeout}。未知发送了多少数据包;因此,只要发生超时,套接字就将关闭(请参见 下面的send_timeout_close)。默认为infinity。 + {send_timeout_close, boolean()} | + 仅允许用于面向连接的套接字。 + 与send_timeout一起使用,以指定当send操作返回{error,timeout}时是否自动关闭套接字。推荐的设置为 true,它将自动关闭套接字。由于向后兼容,默认为false。 + {show_econnreset, boolean()} | + 当此选项设置为false时(默认情况下),将从TCP对等方接收到的RST视为正常关闭(就像已发送FIN一样)。gen_tcp:recv / 2的调用者 获得{错误,关闭}。在活动模式下,控制进程收到 {tcp_closed,Socket}消息,指示对等方已关闭连接。 + 将此选项设置为true可让您区分正常关闭的连接和TCP对等方中止(有意或无意)的连接。调用 gen_tcp:recv / 2 返回{error,econnreset}。在活动模式下,控制过程会在通常的{tcp_closed,Socket}之前接收到 {tcp_error,Socket,econnreset}消息,就像其他套接字错误一样。调用 gen_tcp:send / 2 也会返回{error,econnreset} 当检测到TCP对等体已发送RST时。 + 从gen_tcp:accept / 1返回的已连接套接字 从侦听套接字 继承了show_econnreset设置。 + {sndbuf, integer() >= 0} | + 用于套接字的发送缓冲区的最小大小。鼓励您使用 getopts / 2来检索操作系统设置的大小。 + {tos, integer() >= 0} | + 在实现此功能的平台上设置IP_TOS IP级别选项。行为和允许范围在不同系统之间有所不同。该选项在未实现的平台上被忽略。请谨慎使用。 + {tclass, integer() >= 0} | + 在实现此功能的平台上 设置IPV6_TCLASS IP级别选项。行为和允许范围在不同系统之间有所不同。该选项在未实现的平台上被忽略。请谨慎使用。 + {ttl, integer() >= 0} | + {recvtos, boolean()} | + {recvtclass, boolean()} | + {recvttl, boolean()} | + + option_name() = + active | buffer | delay_send | deliver | dontroute | + exit_on_close | header | high_msgq_watermark | + high_watermark | keepalive | linger | low_msgq_watermark | + low_watermark | mode | nodelay | packet | packet_size | + pktoptions | priority | + {raw,Protocol :: integer() >= 0, OptionNum :: integer() >= 0, ValueSpec ::(ValueSize :: integer() >= 0) | (ValueBin :: binary())} | + recbuf | reuseaddr | send_timeout | send_timeout_close | + show_econnreset | sndbuf | tos | tclass | ttl | recvtos | + recvtclass | recvttl | pktoptions | ipv6_v6only + + connect_option() = + {ip, inet:socket_address()} | + {fd, Fd :: integer() >= 0} | + {ifaddr, inet:socket_address()} | + inet:address_family() | + {port, inet:port_number()} | + {tcp_module, module()} | + {netns, file:filename_all()} | + {bind_to_device, binary()} | + option() + listen_option() = + {ip, inet:socket_address()} | + {fd, Fd :: integer() >= 0} | + {ifaddr, inet:socket_address()} | + inet:address_family() | + {port, inet:port_number()} | + {backlog, B :: integer() >= 0} | + {tcp_module, module()} | + {netns, file:filename_all()} | + {bind_to_device, binary()} | + option() + socket() + As returned by accept/1,2 and connect/3,4. \ No newline at end of file diff --git a/src/docs/erlang性能优化.md b/src/docs/erlang性能优化.md new file mode 100644 index 0000000..25dd32d --- /dev/null +++ b/src/docs/erlang性能优化.md @@ -0,0 +1,215 @@ +#### erlang 各种 优化设置 + 一、 erl启动时参数: + +K true 开启epoll调度,在linux中开启epoll,会大大增加调度的效率 + +A 100 异步线程池,为某些port调用服 + +P 1024000 最大进程数 + +Q 65535 最大port数 + +sbt db 绑定调度器,绑定后调度器的任务队列不会在各个CPU线程之间跃迁,结合sub使用,可以让CPU负载均衡的同时也避免了大量的跃迁发生。 + 将scheduler绑定到具体的cpu核心上,再配合erlang进程和port绑定,可以显著提升性能,但是如果绑定错误,反而会有反效果 + ( 进程调度器绑定:erlang:process_flag(scheduler, 1),当进程使用了port时,还需要port绑定支持,防止进程在不同调度器间迁移引起性能损失,如cache、跨numa node拷贝等,当进程使用了port时,主要是套接字,若进程与port不在一个scheduler上,可能会引发严重的epoll fd锁竞争及跨numa node拷贝,导致性能严重下降) + 注意:一个linux系统中,最好只有一个evm开启此选项,若同时有多个erlang虚拟机在系统中运行,还是关闭为好 + + +sub true 开启CPU负载均衡,false的时候是采用的CPU密集调度策略,优先在某个CPU线程上运行任务,直到该CPU负载较高为止。 + +swct eager 此选项设置为eager后,CPU将更频繁的被唤醒,可以增加CPU利用率 + +spp true 开启并行port并行调度队列,当开启后会大大增加系统吞吐量,如果关闭,则会牺牲吞吐量换取更低的延迟。 + +zdbbl 65536 分布式erlang的端口buffer大小,当buffer满的时候,向分布式的远程端口发送消息会阻塞 + + 二、erlang内部进程启动参数 + 示例:创建一个新进程并进行注册,该进程是全局唯一的自增ID生成进程,因此无法做多进程处理,这个时候单进程的性能就是至关重要的 + 首先,出于性能和功能考虑,这个进程不是gen_server;其次进行了部分参数调优能 + register(num_generator, spawn_opt(?MODULE, init, [],[{priority,high},{scheduler,0},{min_heap_size, 65536 * 2},{min_bin_vheap_size,65536 * 2}])). + 参数讲解: + 1.priority + erlang是公平调度策略,因此默认情况下每个进程得到的运行时间片是相同的:2000reductions,但是对于我们的应用场景来说,这个进程应该是优先级较高的,需要得到更多的调度,因此设置为high,还可以设置为max,但是max是系统进程的预留优先级,用high即可 + 2. scheduler + 将该进程绑定到指定的scheduler上,防止进程的任务被scheduler分配来分配去,可以减少CPU调用,注意这个和+sbt db是不同的,+sbt db是防治调度器的任务队列在CPU线程间跃迁,scheduler是为了防止进程在时间片切换过程中被分配给其它的调度器 + 3.min_heap_size + 进程初始堆大小,用内存换CPU的典型做法,增大初始大小,可以显著降低GC次数和内存再分配次数, 减少处理过程中产生大量term,尤其是list时的gc次数 + 4.min_bin_vheap_size + 进程初始二进制堆大小,当该进程对于binary数据的处理交换很多时,可以获得和增大min_heap_size一样的效果, 减少大量消息到达或处理过程中产生大量binary时的gc次数 + + 三、port(socket)调优 + 示例:服务器监听端口,接受客户端请求。典型应用场景web服务器,需要实现高吞吐,低延迟的目标 + Res = gen_tcp:listen(Port, [binary, + {reuseaddr, true}, + {nodelay, true}, + {delay_send,true}, + {high_watermark,64 * 1024}, + {send_timeout, 30000}, + {send_timeout_close, true}, + {keepalive, true}]) + + 参数详解: + binary: + 接收到客户端的消息后,作为binary来处理,binary在erlang中是很高效的数据结构,超过64字节,就是全局保存的,因此在很多操作下是不需要复制的,仅仅复制binary的指针即可,详细请搜索refc binary,注意:binary大量使用需要有丰富的经验,不然可能会内存泄漏 + reuseaddr: + 允许系统复用port,对于高吞吐的系统,这个参数很重要,请搜索:linux port 复用 + nodelay: + 开启linux中的TCP_NODELAY参数,请搜索:TCP_NODELAY 40毫秒延迟 + delay_send: + 默认的erlang port消息发送,是直接发送,若失败则排队处理,然后由调度器进行队列poll操作,如果设置为true,那么就不尝试直接发送,而且扔进队列,等待poll,开启选项会增加一点点消息延迟,换来吞吐量的大量提升 + high_watermark: + port的发送缓存,缓存满了后,下次发送会直接阻塞,直到缓存低于某个阈值low_watermark。如果是密集网络IO系统,请增大该buffer,避免发送阻塞 + send_timeout: + 在high_watermark中提到了发送阻塞,如果阻塞超过这个时间,那么就会超时,发送直接返回,停止发送 + send_timeout_close: + 如果发生了send_timeout同时设置了send_timeout_close选项,那么超时后,会直接关闭socket.如果发送进程不是很重要,例如web用户进程,强烈建议开启这个选项,当发送30秒超时的时候,就说明该用户出现了很大的麻烦,断开连接是最理想的做法,否则可能出现很多奇怪的bug. + keepalive: + 遵循HTTP/1.1协议的keepalive规定,这个根据业务需求选择是否开启,如果同一个客户端会连续发起http请求,那么建议设置为true,避免多次TCP握手 + 示例:服务器发起大量的http请求,在优化了参数后,同样的吞吐量所耗费的时间是未优化前的1/3 - 1/2(经过严苛的测试得出的数据) + + inets:start(), + httpc:set_options([{max_keep_alive_length,500},{max_sessions,100},{nodelay,true},{reuseaddr,true}]), + + 参数详解: + max_keep_alive_length: + 在同一条http连接上允许发送的最大包数,默认为5,超过5个包,就会重连 + max_sessions: + 跟目标服务器之间最大的并行http连接数目,大大的增加了数据上行吞吐量 + nodelay_true: + 见上文 + reuseaddr: + +   6. 数据结构: +      减少遍历,尽量使用API提供的操作 +      由于各种类型的变量实际可以当做c的指针,因此erlang语言级的操作并不会有太大代价 +      lists:reverse为c代码实现,性能较高,依赖于该接口实现的lists API性能都不差,避免list遍历,[||]和foreach性能是foldl的2倍,不在非必要的时候遍历list +      dict:find为微秒级操作,内部通过动态hash实现,数据结构先有若干槽位,后根据数据规模变大而逐步增加槽位,fold遍历性能低下 +      gb_trees:lookup为微秒级操作,内部通过一个大的元组实现,iterator+next遍历性能低下,比list的foldl还要低2个数量级 + 9. 文件预读,批量写,缓存: + 这些方式都是局部性的体现: + 预读:读空间局部性,文件提供了read_ahead选项 + 批量写:写空间局部性 +  对于文件写或套接字发送,存在若干级别的批量写: +    1. erlang进程级:进程内部通过list缓存数据 +    2. erlang虚拟机:不管是efile还是inet的driver,都提供了批量写的选项delayed_write|delay_send, +       它们对大量的异步写性能提升很有效 +    3. 操作系统级:操作系统内部有文件写缓冲及套接字写缓冲 +    4. 硬件级:cache等 + 缓存:读写时间局部性,读写空间局部性,主要通过操作系统系统,erlang虚拟机没有内部的缓存 + 10.套接字标志设置: + 延迟发送:{delay_send, true},聚合若干小消息为一个大消息,性能提升显著 + 发送高低水位:{high_watermark, 128 * 1024} | {low_watermark, 64 * 1024},辅助delay_send使用,delay_send的聚合缓冲区大小为high_watermark,数据缓存到high_watermark后,将阻塞port_command,使用send发送数据,直到缓冲区大小降低到low_watermark后,解除阻塞,通常这些值越大越好,但erlang虚拟机允许设置的最大值不超过128K + 发送缓冲大小:{sndbuf, 16 * 1024},操作系统对套接字的发送缓冲大小,在延迟发送时有效,越大越好,但有极值 + 接收缓冲大小:{recbuf, 16 * 1024},操作系统对套接字的接收缓冲大小 + +#### Erlang 虚拟机调优 + 目录 + SMP + Schedulers + Port Settings + Asynchronous Thread Pool + Kernel Polling + Warning Messages + Process Limit + Distribution Buffer + Erlang Built-in Storage + Crash Dumps + Net Kernel Tick Time + Shutdown Time + Riak 是用Erlang语言写的,运行在Erlang虚拟机之上.所以Erlang虚拟机的调优对Riak的性能优化就显得尤为重要. Erlang虚拟机本身提供了非常多的配置参数对性能调优, Riak支持其中的一部分参数,你可以在每个node的Riak配置文件中进行设置. + + +下表列出了其中的一部分,左边一列是Erlang中的参数名称, 右边一列是在Riak中的参数名称. +Erlang parameter Riak parameter ++A erlang.async_threads ++K erlang.K ++P erlang.process_limit ++Q erlang.max_ports ++S erlang.schedulers.total, erlang.schedulers.online ++W erlang.W ++a erlang.async_threads.stack_size ++e erlang.max_ets_tables ++scl erlang.schedulers.compaction_of_load ++sfwi erlang.schedulers.force_wakeup_interval +-smp erlang.smp ++sub erlang.schedulers.utilization_balancing ++zdbbl erlang.distribution_buffer_size +-kernel net_ticktime erlang.distribution.net_ticktime +-env FULLSWEEP_AFTER erlang.fullsweep_after +-env ERL_CRASH_DUMP erlang.crash_dump +-env ERL_MAX_ETS_TABLES erlang.max_ets_tables +-name nodename +Note on upgrading to 2.0 +在Riak2.0版本之前, Erlang虚拟机相关的参数放在配置文件 vm.args 里面. 在2.0及之后的版本中, 所有Erlang虚拟机相关的配置参数放在配置文件 riak.conf 里面. 如果你从Riak2.0之前的版本升级到Riak 2.0, 你仍然可以继续使用旧的配置文件 vm.args. 但是, 如果你同时设置了配置文件 vm.args 和riak.conf, 在 vm.args里面的配置将会覆盖riak.conf里面的配置. +##### SMP + 有些操作系统提供Erlang虚拟机对称多处理器能力(SMP)以利用多处理器硬件架构的优势. SMP的支持可以通过设置erlang.smp参数来打开和关闭, 默认是打开的. 下面的例子是关闭SMP的支持. + riak.conf + erlang.smp = disable + 由于Riak也可以运行在一些不支持SMP的操作系统上, 所以在使用之前需要确认操作系统是否支持SMP,如果操作系统本身不支持,那么需要在启动Riak集群之前在配置文件riak.conf中关闭SMP的选项. + + 比较安全的一个选择是把erlang.smp设置成auto, 这个选项会指示Erlang虚拟机启动SMP支持之前检查操作系统是否支持以及是否有一个以上的逻辑处理器,只有这两个条件都满足的时候,Erlang虚拟机才启动SMP支持. + +##### Schedulers + Note on missing scheduler flags + We recommend that all users set the +sfwi to 500 (milliseconds) and the +sclflag to false if using the older, vm.args-based configuration system. If you are using the new, riak.conf-based configuration system, the corresponding parameters are erlang.schedulers.force_wakeup_interval anderlang.schedulers.compaction_of_load. + Please note that you will need to uncomment the appropriate lines in your riak.conf for this configuration to take effect. + 如果在Erlang虚拟机里已经打开了支持SMP的选项, 比如erlang.smp已经被设置成enabled 或者auto,而且机器本身超过一个逻辑处理器同时也支持SMP, 那么当你启动Riak的时候, 你可以配置逻辑处理器的数量或者调度线程的数量,同时也可以设置online线程的数量. + 全部调度线程的数量可以通过参数erlang.schedulers.total来设置, online线程的数量则是通过参数erlang.schedulers.online来配置. 这两个参数可以分别对应到Erlang虚拟机的参数Schedulers 和SchedulersOnline. + 两个参数的最大值都是1024, 参数并没有统一的默认值. 但是, Erlang 虚拟机自己会尝试去判定有多少配置的CPU(core)和可用的CPU(core). 如果Erlang虚拟机能够做出这个判定,那么参数schedulers.total会默认设置成配置的CPU(core)数量, + 参数schedulers.online会默认设置成可用的CPU(core)数量. 但是, 如果Erlang虚拟机不能做出判定, 两个参数的默认值将会设置成1. + 如果两个参数中的任意一个被设置成负数, 那么意味着这个参数值将会被设成默认配置的处理器数量(如果scheduler.total是负数)或者可用的处理器数量(如果schedulers.online是负数) 减去配置的负值. 比如, 如果机器配置有100个cpu(cores)然后参数schedulers.total配置为-50, 计算以后的值就是50. + 如果两个参数中的任意一个被设置为0,两个值都会被重新设为默认值. + 如果SMP支持被关闭, 比如erlang.smp被设成disabled或者设成auto 但是机器本身不支持SMP或者机器只有一个逻辑处理器,那么两个参数schedulers.total 和 schedulers.online都将会被忽略. + +Scheduler Wakeup Interval +调度器唤醒是一个可选处理, 通过这个Erlang 虚拟机调度器被周期性的扫描来判定是否已经陷入睡眠, 比如是否调度器有一个空的运行列表. 这个扫描时间间隔可以通过参数erlang.schedulers.force_wakeup_interval设置, 单位为毫秒.这个参数对应于Erlang虚拟机的+sfwi选项.该参数默认设为0, 不激活调度器唤醒功能. +Erlang在R15Bx版本里有把调度器睡眠过于频繁的倾向,如果你使用的是更新的版本,比如Riak2.0 及以后, 那多数情况下不需要启动唤醒功能. +注: OTP的工程师曾经解释过这个功能,如果需要调度的任务不是很多,没有很多task在运行列表上的话, R15B的Erlang虚拟机会倾向于把这些task尽量集中到尽可能少的调度器上来调度, 睡眠没有调度任务的调度器, 这样可以减少调度器之间的通信花费overhead, 提高CPU的利用率. 但这个也是一个trade off, 具体还是需要用户来根据自己的实际环境来调优. 因为一旦task的数量增加比较多,或者task数量没有增加但是task本身比较耗时,那么很可能就会触发调度器的唤醒, 而唤醒调度器是比较expensive的操作, 如果频繁睡眠唤醒的话,可能会得不偿失. + +##### Scheduler Compaction and Balancing + Erlang调度器提供了两种方式来分发负载到不同的调度器上, 集中负载和utilization balancing. + 集中负载是默认打开的, 打开的时候Erlang虚拟机会尝试去尽可能多的使调度器繁忙,比如通过把任务集中到有限的几个调度器上(假设这几个有限的调度器充分运行的情况下可以调度完目前的tasks)使这几个调度器一直有工作做(not run out of work). 为了达到这个目的, 当虚拟机分配任务的时候会考虑哪些调度器应该被分配任务. 用户可以设置参数erlang.schedulers.compaction_of_load为false来关闭这个功能. + 另外一个选项, utilization balancing, 为了支持负载平衡, 默认是关闭的. 如果打开了这个选项, Erlang虚拟机则努力在不同调度器之间平衡调度器的利用. 如果不考虑每个调度器没有任务可调度的频度的话, 可以打开这个设置, erlang.schedulers.utilization_balancing 设为true(老版本里面通过设置+scl false) + 在任何时候, 只可以是使用两个功能中的一个. 如果同时设置这两个选项为false的话, Riak 会默认使用集中负载选项.如果同时设置为true, Riak会使用那个在配置文件riak.conf中最先出现的那个.(如果是旧版本的话,配置文件会是vm.args) + +##### Port Settings +Riak 使用epmd, Erlang 端口映射Daemon来进行大多数的节点间的通信. 在这个系统里, 集群里的其他节点使用由nodename参数(或者是name in vm.args)来作为节点ID. 比如, riak@10.9.8.7. 在每个节点上, daemon把这些节点ID解析成一个TCP的端口. 用户可以指定一个端口范围给Riak节点来监听使用,同时也可以知道最大数量的并ports/sockets. +Port Range +默认情况下 , epmd绑定到TCP端口4369上并且侦听通配符接口. epmd 默认使用一个不能预测的端口作为节点间的通信, 通过绑定到端口0上, 意味着会使用第一个可用的端口. 这样就使得防火墙非常难配置. +为了是防火墙配置简化, 用户可以指导Erlang虚拟机使用一个有限范围的端口或者单一端口. 这个最小和最大值可以设置在参数erlang.distribution.port_minimum和erlang.distribution.port_maximum里面. 比如, 下面的值被设为3000和5000. +riak.conf +app.config +erlang.distribution.port_range.minimum = 3000 +erlang.distribution.port_range.maximum = 5000 +用户可以设置Erlang虚拟机使用一个单一端口, 如果只设置了最小值没有设置最大值,则表示使用单一端口. 比如, 下面设置使用单一端口5000. +riak.conf +app.config +erlang.distribution.port_range.minimum = 5000 +如果最小端口没有设置, Erlang虚拟机将会在随机的高编号端口上侦听. + +##### Maximum Ports +用户可以通过设置参数erlang.max_ports来指定Erlang虚拟机可以使用的最大并发的 ports/sockets数量, 范围从1024到134217727. 默认值是65536. 在vm.args里面对应的参数是+Q 或者-env ERL_MAX_PORTS. +Asynchronous Thread Pool +如果Erlang虚拟机支持线程可用, 用户可以为Erlang虚拟机设置异步线程池的线程数量, 使用参数erlang.async_threads(+A in vm.args). 线程数量范围从0至1024, 默认值是64,下面的例子是设置成600的情况. +riak.conf +vm.args +erlang.async_threads = 600 + + +##### Stack Size +除了可以指定异步线程的数量之外, 用户还可以为每个异步线程指定stack size. 参数是erlang.async_threads.stack_size, 对应到Erlang的+a参数. 用户可以在Riak中为这个参数指定size以KB, MB,GB 为单位, 有效的范围值是16至8192个字, 在32位的系统上就是64至32768字节. 该参数没有默认值, 我们建议设置为16K words, 对应为64 KB在32位系统上. 我们建议这么小一个值是考虑到异步线程数量可能会很大. +注:The 64 KB default is enough for drivers delivered with Erlang/OTP but might not be large enough to accommodate drivers that use the driver_async()functionality, documented here. We recommend setting higher values with caution, always keeping the number of available threads in mind. +Kernel Polling +如果系统支持, 用户可以在Erlang中利用内核轮询. 内核轮询可以在使用很多文件描述符的时候提高性能. 在使用中的文件描述符越多, 内核轮询发挥的作用就越大. 该选择在Riak的Erlang虚拟机中是默认打开的, 该参数对应到Erlang虚拟机中的+K参数 + +##### Warning Messages +Erlang虚拟机的error_logger 是一个事件管理器, 从Erlang运行时系统注册错误, 告警和信息事件. 默认情况下, error_logger的信息事件被映射为告警,但是用户可以设置映射成错误或者信息. 该设置为参数erlang.W, 可以设置的值为w(warning), errors 或者i(info reports). + +##### Process Limit +参数erlang.process_limit可以用来设置系统同时存在的最大进程数量(对应到Erlang的+P参数), 有效范围从1024至134217727. 默认值是256000. + +##### Distribution Buffer +用户可以通过参数erlang.distribution_buffer_size设置Erlang虚拟机的distribution buffer busy limit(对应到Erlang的+zdbbl参数). 修改这个参数对那些有许多busy dist port事件的节点可能会有帮助, 默认值是32MB, 最大值是2097151KB. 增大这个参数可以允许进程缓存更多的待发消息, 当缓存满的时候,发送线程被挂起直到缓存减小到设定值. 所以, 更大的缓存有助于降低延迟以及增加吞吐量,代价就是使用了更多的RAM. 用户需要根据机器的RAM资源来考虑设定这个值. + +##### Erlang Built-in Storage +Erlang使用一个内置的数据库,ets(Erlang Term Storage)用来快速访问内存(constant access time rather than logarithmic access time). erts 表的最大数量设置在参数erlang.max_erts_tables里面, 默认值是256000,这个值要大于Erlang虚拟机自身的默认值1400(对应到vm.args 的参数e). 更大的erlang.max_erts_tables值可以提供更快的数据访问,代价是消耗更高的内存. + +##### Crash Dumps +默认情况下, Riak 的Erlang crash dumps文件是存放在位置./log/erl_crash.dump. 用户可以通过设置参数erlang.crash_dump来更改存放位置. 该参数对应到Erlang虚拟机的ERL_CRASH_DUMP环境变量. + +##### Net Kernel Tick Time +网络内核是Erlang的一个系统进程, 提供了不同的网络监视形式. 在一个Riak集群里面, 网络内核的功能之一就是去周期性的检测节点存活. Tick time就是这个检查频度, 可以通过erlang.distribution.net_ticktime设置,单位是秒. 该参数对应到vm.args里面的参数-kernal net_ticktime. + +##### Shutdown Time +用户可以设定Erlang虚拟机的关闭时间, 该设置参数为erlang.shutdown_time,默认是10秒, 一旦10秒过了, 所有存在的进程就会被杀掉. 减少关闭时间在某些情景下可能是有帮助的, 比如说在测试的时候需要频繁的启停Riak集群. 在vm.args里参数是shutdown_time, 单位是毫秒. \ No newline at end of file diff --git a/src/docs/erlang规范.md b/src/docs/erlang规范.md new file mode 100644 index 0000000..7320bb2 --- /dev/null +++ b/src/docs/erlang规范.md @@ -0,0 +1,1159 @@ +Erlang 编码标准指引 +==================================== +Table of Contents: +* [约定 & 规则](#约定--规则) + * [源码布局](#源码布局) + * [用空格代替制表符(tab)](#用空格代替制表符(tab)) + * [使用你的空格键](#使用你的空格键) + * [行尾不要留空格](#行尾不要留空格) + * [每行100列](#每行100列) + * [保持现有风格](#保持现有风格) + * [避免多层嵌套](#避免多层嵌套) + * [更多, 小函数比 case 表达式好用](#更多-小函数比-case-表达式好用) + * [函数按逻辑功能分组](#函数按逻辑功能分组) + * [集中你的 types](#集中你的-types) + * [不要上帝模块](#不要上帝模块) + * [Honor DRY](#抽象重复代码) + * [避免动态调用](#避免动态调用) + * [语法](#语法) + * [避免使用 if 表达式](#避免使用-if-表达式) + * [避免嵌套 try...catches](#避免嵌套try...catches) + * [命名](#命名) + * [在命名概念时保持一致](#在命名概念时保持一致) + * [Don't use _Ignored variables](#不要使用匿名变量) + * [避免用布尔类型作为函数参数](#避免用布尔类型原子作为函数参数) + * [原子(atoms)请用小写](#原子(atoms)请用小写) + * [函数名](#函数名) + * [变量名](#变量名) + * [宏](#宏) + * [宏的应用场景](#宏的应用场景) + * [宏名要大写](#宏名要大写) + * [记录(Records)](#记录(Records)) + * [记录命名](#记录命名) + * [在 specs 里避免出现记录(record)](#在specs里避免出现记录) + * [给记录添加类型Types](#给记录添加类型Types) + * [其它](#其它) + * [给函数添加-spec函数规范定义](#给函数添加-spec函数规范定义) + * [模块中不要用import](#模块中不要用import) + * [Don't Use Case Catch](#Don't Use Case Catch) +* [好的建议和方法](#好的建议和方法) + * [优先使用高级函数而不是手写的递归方法](#优先使用高级函数而不是手写的递归方法) + * [驼峰式命名,下划线命名](#驼峰式命名,下划线命名) + * [更短 (但仍保持有意义的) 的变量名称](#更短(但仍保持有意义的)的变量名称 ) + * [注释等级](#注释等级) + * [保持函数精简](#保持函数精简) + * [避免不必要调用length/1](#避免不必要调用length/1 ) + +### 约定--规则 +### 源码布局 + +#### 用空格代替制表符(tab) +> 用空格代替制表符(tab),使用两个空格符作为缩进. +*Examples*: [indent](src/indent.erl) +```erlang +%% @doc 不一致 +bad() -> + try + ThisBlock = is:indented(with, two, spaces), + that:is_good(ThisBlock) %% 这一部分的代码缩进用两个空格,没啥毛病 + catch + _:_ -> + this_block:is_indented(with, four, spaces) %% 但是这一部分的却用了4个空格,看起来不统一,很糟糕 + end. + +%% @doc 一致,但是使用4个空格 +better() -> + receive + {this, block} -> is:indented(with, four, spaces); + _That -> is:not_good() %% 这一部分的代码缩进用四个空格,不太好 + after 100 -> + but:at_least(it, is, consistent) %% 但起码全部是使用一致的风格 + end. + +%% @doc 不错 +good() -> + case indentation:block() of + {2, spaces} -> me:gusta(); + {_, _} -> not_sure:if_gusta() + end. +``` + +*原因*: 这并不意味着允许代码中存在多层嵌套的结构.如果代码足够干净,2个空格就足够了,代码看起来更加简洁,同时在同一行中也能容纳更多的字符. + +*** +#### 使用你的空格键 +> 使用空格来分割开运算符和逗号. + +*Examples*: [spaces](src/spaces.erl) +```erlang +% @doc 没有空格 +bad(_My,_Space,_Bar)->[is,'not',working]. + +% @doc 带空格!! +good(_Hey, _Now, _It) -> ["works " ++ "again, " | [hooray]]. +``` + +*原因*: 同上,主要是为了代码易于读写,等等. 在这里顺便提醒一下erlang宏展开的时候会自动在两边增加分隔符 +*Examples*: -define(plus,+). +t(A,B) -> A?plus+B. +结果会是这样的: +t(A,B) -> A + + B. +而不是这样的: +t(A,B) -> A ++ B. + +*** +#### 行尾不要留空格 +> 检查你的没一行代码的最后,不要有空格. + +*Examples*: [trailing_whitespace](src/trailing_whitespace.erl) + +```erlang +bad() -> "这行尾部有空格". + +good() -> "这行没有". +``` + +*原因*: 这是提交噪音. 可以看看[长篇论据](https://programmers.stackexchange.com/questions/121555/why-is-trailing-whitespace-a-big-deal). + +#### 每行100列 +> 每行最多100个字符. + +*Examples*: [col_width](src/col_width.erl) + +```erlang +%$ @doc 太宽 +bad([#rec{field1 = FF1, field2 = FF2, +field3 = FF3}, #rec{field1 = BF1, field2 = BF2, field3 = BF3} | Rest], Arg2) -> + other_module:bad(FF1, FF2, FF3, BF1, BF2, BF3, bad(Rest, Arg2)). + +%% @doc 不错 (< 100 字符) +good([Foo, Bar | Rest], Arg2) -> + #rec{field1 = FF1, field2 = FF2, field3 = FF3} = Foo, + #rec{field1 = BF1, field2 = BF2, field3 = BF3} = Bar, + other_module:good(FF1, FF2, FF3, BF1, BF2, BF3, good(Rest, Arg2)). +``` + +*原因*:太长的行在处理的时候是相当痛苦的: 要么在编辑的时候不停水平滚动, 要么就是忍受自动断行造成布局错乱. +100个字符的限制不仅仅让每一行保持简短, 另外也能让你可以毫无压力地在标准的手提电脑屏幕上并排同时打开两个文件, 或者三个 1080p 显示器上. + +*** +#### 保持现有风格 +> 当你维护别人的模块时, 请坚持按前人的编码风格样式维护. 如果项目有整体的风格样式, 那么在编写新的模块是也要坚持按项目的整体风格进行. + +*Examples*: [existing_style](src/existing_style.erl) + +```erlang +bad() -> + % 之前的代码 + List = [ {elem1, 1} + , {elem2, 2} + % 新代码 (不按之前的格式来编码) + , {elem3, 3}, {elem4, 4}, + {elem5, 5} + ], + other_module:call(List). +good() -> + % 之前的代码 + List = [ {elem1, 1} + , {elem2, 2} + % 新代码 (按之前的格式来编码) + , {elem3, 3} + , {elem4, 4} + , {elem5, 5} + ], + other_module:call(List). +``` + +*原因*: 在维护别人的代码的时候,如果你不喜欢他的编码规范,这仅仅是你个人不喜欢而已,但是如果你不按他之前写的编码样式继续编写, +那这个模块就有两种编码样式了,这样你本人看起来这些代码很丑陋,别人看你的代码也觉得很丑陋,这样会让代码更加不容易维护. + +*** + +#### 避免多层嵌套 +> 尽量不要出现超过三个层级嵌套的代码样式 + +*Examples*: [nesting](src/nesting.erl) + +```erlang +bad() -> + case this:function() of + has -> + try too:much() of + nested -> + receive + structures -> + it:should_be(refactored); + into -> + several:other(functions) + end + catch + _:_ -> + dont:you("think?") + end; + _ -> + i:do() + end. + +good() -> + case this:function() of + calls -> + other:functions(); + that -> + try do:the(internal, parts) of + what -> + was:done(in) + catch + _:the -> + previous:example() + end + end. + +%% 译者注: 上面部分代码的意思:通过将嵌套部分的代码封装成一些新的函数,可以减少嵌套的结构. +``` + +*原因*: 嵌套级别表示函数中的逻辑比较复杂,过多地将需要执行和完成的决策放在单个函数中. 这不仅阻碍了可读性,而且阻碍了可维护性,如果嵌套过多梳理逻辑 +分支代码也很容易看错拆分成相应的函数可读性更好,逻辑也会更清晰,也便于调试以及编写单元测试的进行, + +*** +#### 更多-小函数比-case-表达式好用 +> 使用模式匹配的函数子句代替 case 表达式. 特别是当 case 在: +> - 函数的开头(下面代码第一个bad函数) +> - case分支比较多的时候 + +*Examples*: [smaller_functions](src/smaller_functions.erl) + +```erlang +%% @doc 这个函数仅仅使用的是 case 表达式 +bad(Arg) -> + case Arg of + this_one -> should:be(a, function, clause); %% 这一句应该用一个函数子句代替 + and_this_one -> should:be(another, function, clause) %% 这一句应该用另一个函数子句代替 + end. + +%% @doc 使用模式匹配 +good(this_one) -> is:a(function, clause); %% 这是一个函数子句 +good(and_this_one) -> is:another(function, clause). %% 这是另一个函数子句 + + +%% @doc case 表达式在函数内部 +bad() -> + InitialArg = some:initial_arg(), + InternalResult = + case InitialArg of + this_one -> should:be(a, function, clause); + and_this_one -> should:be(another, function, clause) + end, + some:modification(InternalResult). + +%% @doc 使用多个函数字句代替内部 case 表达式 +good() -> + InitialArg = some:initial_arg(), + InternalResult = good(InitialArg), + some:modification(InternalResult). +``` + +*原因:* 一般而言,函数体中的一个case代表某种决定,同时函数应尽可能的简单. 如果决策结果的每个分支作为一个函数子句而不是一个case子句来实现, +同时函数子句的函数名也可以让代码容易读懂. 换言之, 这个 case 在此扮演的是 '匿名函数', 除非它们在高阶函数的上下文中被使用,而只是模糊的含义. + +*** +#### 函数按逻辑功能分组 +> 始终保持区分导出函数和未导出的函数, 并将导出的放在前面, 除非还有其他方法更加有助于可读性和代码发现的. + +*Examples*: [grouping_functions](src/grouping_functions) + +`bad.erl`: + +```erlang +%%% @doc 私有和公用函数随意摆放 +-module(bad). + +-export([public1/0, public2/0]). + +public1() -> private3(atom1). + +private1() -> atom2. + +public2() -> private2(private1()). + +private2(Atom) -> private3(Atom). + +private3(Atom) -> Atom. +``` + +`better.erl`: + +```erlang +%%% @doc 按函数相关程度区分组 +-module(better). + +-export([public1/0, public2/0]). + +public1() -> + case application:get_env(atom_for_public_1) of + {ok, X} -> public1(X); + _ -> throw(cant_do) + end. +%% @doc 这是一个仅仅与上面函数相关的私有函数 +public1(X) -> private3(X). + +public2() -> private2(private1()). + +private1() -> atom2. + +private2(Atom) -> private3(Atom). + +private3(Atom) -> Atom. +``` + +`good.erl`: + +```erlang +-module(good). + +-export([public1/0, public2/0]). + +public1() -> + case application:get_env(atom_for_public_1) of + {ok, X} -> private3(X); + _ -> throw(cant_do) + end. + +public2() -> private2(private1()). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% PRIVATE FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +private1() -> atom2. + +private2(Atom) -> private3(Atom). + +private3(Atom) -> Atom. +``` + +*原因*: 好的代码结构易于读/理解/修改,很多时候在写erlang代码的时候写着写着发现需要添加一些额外的分支匹配函数, +有时候这种分支匹配函数就为了某种情况下使用,有可能就用一次,这时候我的习惯就是把这个分支匹配函数写在要用这个函数的函数前面 + +*** +#### 集中你的 types +> 将 types 都放在文件开头的地方 + +*Examples*: [type_placement](src/type_placement.erl) + +```erlang +-type good_type() :: 1..3. + +-spec good() -> good_type(). +good() -> 2. + + +-type bad_type() :: 1..3. +-spec bad() -> bad_type(). +bad() -> 2. +``` + +*原因*: Types 定义的数据结构极有可能被用于多个函数,所以他们的定义不能只与其中一个有关. +另外将他们在代码中放在一起并像文档一样展示他们就像edoc 也是将 types 放在每个文档的开头一样. + +*** +#### 不要上帝模块 +> 不要让你的系统使用上帝模块 (模块中包含了很多函数 和/或 函数与函数之间处理的事情并不相关) + +*Examples*: [god](src/god.erl) + +```erlang +%%% @doc all of your db operations belong to us! +-module(god). + +-export([create_user/1, create_user/2, create_user/3]). +-export([update_user/2, update_user/3]). +-export([delete_user/1]). +-export([create_post/1, create_post/2, create_post/3]). +-export([update_post/2, update_post/3]). +-export([delete_post/1]). +-export([create_comment/2, create_comment/3]). +-export([update_comment/3, update_comment/4]). +-export([delete_comment/2]). + +create_user(Name) -> create_user(Name, undefined). + +create_user(Name, Email) -> create_user(Name, Email, undefined). + +create_user(Name, Email, Phone) -> + some_db:insert(users, [{name, Name}, {email, Email}, {phone, Phone}]). + +update_user(Name, Changes) -> + some_db:update(users, [{name, Name}], Changes). + +update_user(Name, Key, Value) -> + update_user(Name, [{Key, Value}]). + +delete_user(Name) -> + some_db:delete(users, [{name, Name}]). + +create_post(Text) -> create_post(Text, undefined). + +create_post(Text, Title) -> create_post(Text, Title, undefined). + +create_post(Text, Title, Image) -> + some_db:insert(posts, [{text, Text}, {title, Title}, {image, Image}]). + +update_post(Text, Changes) -> + some_db:update(posts, [{text, Text}], Changes). + +update_post(Text, Key, Value) -> + update_post(Text, [{Key, Value}]). + +delete_post(Text) -> + some_db:delete(posts, [{text, Text}]). + +create_comment(PostId, Text) -> create_comment(PostId, Text, undefined). + +create_comment(PostId, Text, Image) -> + some_db:insert(comments, [{post_id, PostId}, {text, Text}, {image, Image}]). + +update_comment(PostId, CommentId, Changes) -> + some_db:update(comments, [{post_id, PostId}, {id, CommentId}], Changes). + +update_comment(PostId, CommentId, Key, Value) -> + update_comment(PostId, CommentId, [{Key, Value}]). + +delete_comment(PostId, CommentId) -> + some_db:delete(comments, [{post_id, PostId}, {id, CommentId}]). +``` + +*原因*: 上帝模块, 类似上帝对象, 了解过多或者负责过多的模块. 上帝模块通常是因为不断的增加功能函数演变出来的. +A beautiful, to-the-point module with one job, one responsibility done well, gains a function. Then another, which does the same thing but with different parameters. +总有一天, 你会写出一个包含500多个函数并且高达6000多行代码的模块 .因此,让模块(和功能)只做一件事情就可以很容易地探索和理解代码,从而维护它. +这个的意思就是按功能拆分模块,不同功能让放到不同模块实现,A模块做A功能相关的事情,B模块做B模块相关的事情,不要把不相关的功能放到一个模块去,特别是作为较底层的模块 + + +*** +#### 抽象重复代码 +> 不要在多个地方使用相同的代码,请用函数或者变量去代替。 +1 把重复的代码抽象成函数 +2 把同个作用域同个函数(参数也一样)的结果用变量保存,替换后面再次调到该函数的地方 +*Examples*: [dry](src/dry.erl) + +```erlang +%% @doc this is a very very trivial example, DRY has a much wider scope but it's +%% provided just as an example +bad() -> + case somthing:from(other, place) of + {show, _} -> + display:nicely(somthing:from(other, place)); + nothing -> + display:nothing() + end. + +good() -> + case somthing:from(other, place) of + {show, _} = ThingToShow -> + display:nicely(ThingToShow); + dont_show_me -> + display:nothing() + end. +``` + +*原因*: 这是一条特别的规约,因为这样子审查人员就可以拒绝接受那些好几个地方都包含相同代码的提交(PRs)了,或者接受那些在某个地方已完成的可复用新实现。 + +*** +#### 避免动态调用 +> If there is no specific need for it, don't use dynamic function calling. + +*Examples*: [dyn_calls](src/dyn_calls.erl) + +```erlang +bad(Arg) -> + Mods = [module_1, module_2, module_3], + Fun = my_function, + lists:foreach( + fun(Mod) -> + Mod:Fun(Arg) + end, Mods). + +good(Arg) -> + mdoule_1:my_function(Arg), + module_2:my_function(Arg), + module_3:my_function(Arg). +``` + +*原因*: Dynamic calls can't be checked by [``xref``](http://erlang.org/doc/apps/tools/xref_chapter.html), +one of the most useful tools in the Erlang world. ``xref`` is a cross reference checking/observing tool. +Xref是一个交叉引用工具,可用于查找函数,模块,应用程序和发行版之间的依赖关系。它通过分析定义的函数和函数调用来实现 + + +*原因*: 不要写面条式代码很难阅读, 理解和修改. The function callgraph for your program should strive to be a directed acyclic graph. + +### 语法 +Erlang语法很可怕, 我说得对吗? 所以你也可以充分利用它, 对吗? _对_? + +*** +#### 避免使用 if 表达式 +> Don't use `if`. + +*Examples*: [no_if](src/no_if.erl) + +```erlang +bad(Connection) -> + {Transport, Version} = other_place:get_http_params(), + if + Transport =/= cowboy_spdy, Version =:= 'HTTP/1.1' -> + [{<<"connection">>, utils:atom_to_connection(Connection)}]; + true -> + [] + end. + + +better(Connection) -> + {Transport, Version} = other_place:get_http_params(), + case {Transport, Version} of + {cowboy_spdy, 'HTTP/1.1'} -> + [{<<"connection">>, utils:atom_to_connection(Connection)}]; + {_, _} -> + [] + end. + +good(Connection) -> + {Transport, Version} = other_place:get_http_params(), + connection_headers(Transport, Version, Connection). + +connection_headers(cowboy_spdy, 'HTTP/1.1', Connection) -> + [{<<"connection">>, utils:atom_to_connection(Connection)}]; +connection_headers(_, _, _) -> + []. +``` + +*原因*: 在某些情况下,`if`会在代码中引入静态布尔逻辑,从而降低代码的灵活性。在其他情况下, +`case`或在其子句中具有模式匹配的函数调用是更具说明性。 对于新手(已经学会在其他语言中使用`if`), +Erlang的“if”可能难以理解或容易被滥用。 + +*更多相关的讨论看下面*: +- [From OOP world](http://antiifcampaign.com/) +- [In this repo](issues/14) +- [In erlang-questions](http://erlang.org/pipermail/erlang-questions/2014-September/080827.html) + +*** +#### 避免嵌套try...catches +> Don't nest `try…catch` clauses + +*Examples*: [nested_try_catch](src/nested_try_catch.erl) + +```erlang +bad() -> + try + maybe:throw(exception1), + try + maybe:throw(exception2), + "We are safe!" + catch + _:exception2 -> + "Oh, no! Exception #2" + end + catch + _:exception1 -> "Bummer! Exception #1" + end. + +good1() -> + try + maybe:throw(exception1), + maybe:throw(exception2), + "We are safe!" + catch + _:exception1 -> + "Bummer! Exception #1"; + _:exception2 -> + "Oh, no! Exception #2" + end. + +good2() -> + try + maybe:throw(exception1), + a_function:that_deals(with, exception2), + "We are safe!" + catch + _:exception1 -> + "Bummer! Exception #1" + end. +``` +*原因*: 嵌套`try ... catch`块会破坏它们的整个目的,即将处理错误的代码与处理预期执行路径的代码隔离开来。 +### 命名 + +*** +#### 在命名概念时保持一致 +> 对于相同的概念,在任何地方都使用相同的变量名 (即使在不同的模块当中). + +*Examples*: [consistency](src/consistency.erl) + +```erlang +bad(UserId) -> internal_bad(UserId). + +internal_bad(User_Id) -> internal_bad2(User_Id). + +internal_bad2(Usr) -> db:get_by_id(Usr). + + +good(UserId) -> internal_good(UserId). + +internal_good(UserId) -> internal_good2(UserId). + +internal_good2(UserId) -> db:get_by_id(UserId). +``` + +*原因*: 当要找出所有用到``OrgID`` 的代码 (例如 我们想把变量从 ``string`` 转为 ``binary``), +我们只要搜索名为 ``OrgID``的变量,而不需要查找所有有可能关于 ``OrgID``的命名变量. +对于这个还是要注意 相同的概念 这个限定 比如我们经常用到的等级 玩家等级 装备等级 技能等级 公会等级 虽然都是等级,但是最好前缀..level +类似还有一些同类型枚举定义加个前缀,字典原子名定义可以加个 pd_ 前缀 + +*** +#### 不要使用匿名变量 +> 以_开头的变量仍然是变量,并且是匹配和绑定的,_开头的变量只是在不使用它的时候避免编译器产生警告信息。如果将_添加到变量的名称,请不要使用它。 +同时即使下划线开头的变量,在后面代码中不使用,但是应该还是需要把下线线后面的变量名写成好,一是为了可读性,二 当修改需要使用该比变量的时候直接去掉下划线 +三 即使是_划线开始 ,但是这个变量名还是被绑定了的 同个函数内不能和其他下划线开头的变量一样 +*Examples*: [ignored_vars](src/ignored_vars.erl) + +```erlang +bad(_Number) -> 2 * _Number. + +good(Number) -> 2 * Number. +``` + +*原因*: They are **not** supposed to be used. + +*** +#### 避免用布尔类型原子作为函数参数 +> Don't use boolean parameters (i.e. `true` and `false`) to control clause selection. + +*Examples*: [boolean_params](src/boolean_params.erl) + +```erlang +bad(EdgeLength) -> bad_draw_square(EdgeLength, true). + +bad_draw_square(EdgeLength, true) -> + square:fill(square:draw(EdgeLength)); +bad_draw_square(EdgeLength, false) -> + square:draw(EdgeLength). + +good(EdgeLength) -> good_draw_square(EdgeLength, full). + +good_draw_square(EdgeLength, full) -> + square:fill(square:draw(EdgeLength)); +good_draw_square(EdgeLength, empty) -> + square:draw(EdgeLength). +``` + +*原因*: 主要目的在于,使用其他原子做匹配时意图清晰,不要求读者检查功能定义以了解其功能。 + +*** +#### 原子(atoms)请用小写 +> 原子命名只能使用小写字母. 当一个原子含有多个单词时 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +,单词之间用 `_` 隔开. 特殊情况可以允许用大写 (例如 `'GET'`, `'POST'`, 等等) + 但是尽量还是控制在一定使用量. + +*Examples*: [atoms](src/atoms.erl) + +```erlang +bad() -> ['BAD', alsoBad, bad_AS_well]. + +good() -> [good, also_good, 'good@its.mail']. +``` + +*原因*: 坚持一个约定使得更容易在代码周围没有“重复”原子。 此外,不使用大写字母或特殊字符减少了对原子周围的需求。 +*** +#### 函数名 +> 函数名称只能使用小写字符或数字。 函数名中的单词必须用`_`分隔。 + +*Examples*: [function_names](src/function_names.erl) + +```erlang +badFunction() -> {not_allowed, camel_case}. + +'BAD_FUNCTION'() -> {not_allowed, upper_case}. + +good_function() -> ok. + +base64_encode() -> ok. +``` + +*原因*: 函数名称是原子,它们应遵循适用于它们的相同规则。 + +*** +#### 变量名 +> 使用驼峰式命名变量. 单词之间不要用下划线分割. + +*Examples*: [variable_names](src/variable_names.erl) + +```erlang +bad(Variablename, Another_Variable_Name) -> + [Variablename, Another_Variable_Name]. + +good(Variable, VariableName) -> + [Variable, VariableName]. +``` + +*原因*:遵循一个约定可以更容易地在代码周围没有“重复”变量。 Camel-case使变量名称在视觉上与原子更加明显,并且符合OTP标准。 + +大部分从其他语言转过来可能都习惯了驼峰命名法,可能对函数名 原子也喜欢用, +但是看很多erlang的开源项目,包括OTP自身的代码命名风格的话都是遵循上面这些规则 + +### 宏 + +*** +#### 宏的应用场景 +> 除了包含以下使用方式的情况外,不要使用宏 +> * 预定义部分: ``?MODULE``, ``?MODULE_STRING`` and ``?LINE`` +> * 魔术数字: ``?DEFAULT_TIMEOUT`` + +*Examples*: [macros](src/macros.erl) +```erlang +-module(macros). + +-define(OTHER_MODULE, other_module). +-define(LOG_ERROR(Error), + error_logger:error_msg( + "~p:~p >> Error: ~p~n\tStack: ~p", + [?MODULE, ?LINE, Error, erlang:get_stacktrace()])). + +-define(HTTP_CREATED, 201). + +-export([bad/0, good/0]). + +bad() -> + try + ?OTHER_MODULE:some_function(that, may, fail, 201) + catch + _:Error -> + ?LOG_ERROR(Error) + end. + +good() -> + try + other_module:some_function(that, may, fail, ?HTTP_CREATED) + catch + _:Error -> + log_error(?LINE, Error) + end. + +log_error(Line, Error) -> + error_logger:error_msg( + "~p:~p >> Error: ~p~n\tStack: ~p", + [?MODULE, Line, Error, erlang:get_stacktrace()]). +``` + +*原因*: 宏的使用不利于调试工作的进行. 如果你尝试用它们来避免重复的代码块,可以使用以下函数去实现。 +具体看 [related blog post](https://medium.com/@erszcz/when-not-to-use-macros-in-erlang-1d3f10d377f#.xc9b4bsl9) by [@erszcz](https://github.com/erszcz). + +*** +#### 宏名要大写 +> 宏名应以大写字母命名: + +*Examples*: [macro_names](src/macro_names.erl) +```erlang +-module(macro_names). + +-define(bad, 1). +-define(BADMACRONAME, 2). +-define(Bad_Macro_Name, 3). +-define(Bad_L33t_M@Cr0, 4). + +-define(GOOD, 5). +-define(GOOD_MACRO_NAME, 6). +``` + +*原因*: 这样做可以区分开普通变量和宏,在使用`grep`等工具查找这个宏时不会出现重复宏名,让查找变得更加容易等好处 + + +### 记录(Records) + +*** +#### 记录命名 +> 记录(`record`)命名只能使用小写字母. 单词之间用 `_`分隔. 这个规则同样适用于`record`的字段名 + +*Examples*: [record_names](src/record_names.erl) + +```erlang +-module(record_names). + +-export([records/0]). + +-record(badName, {}). +-record(bad_field_name, {badFieldName :: any()}). +-record('UPPERCASE', {'THIS_IS_BAD' :: any()}). + +-record(good_name, {good_field_name :: any()}). + +records() -> [#badName{}, #bad_field_name{}, #'UPPERCASE'{}, #good_name{}]. +``` + +*原因*: `record`和其字段名都是原子(`atom`), 因此跟原子的命名规则是一样的. + + +#### 在specs里避免出现记录 +> 在 specs 里应该尽可能用 `types` 代替 记录(`records`). + +*Examples*: [record_spec](src/record_spec.erl) + +```erlang +-module(record_spec). + +-record(state, {field1:: any(), field2:: any()}). + +-opaque state() :: #state{}. + +-export_type([state/0]). + +-export([bad/1, good/1]). + +-spec bad(#state{}) -> {any(), #state{}}. +bad(State) -> {State#state.field1, State}. + +-spec good(state()) -> {any(), state()}. +good(State) -> {State#state.field1, State}. +``` + +*原因*: 类型可以导出使用,同时也有助于文档化, 使用 ``opaque`` 可以对记录进行封装和抽象. + +*** +#### 给记录添加类型Types +> 保持给记录(`record`)的每个字段添加类型定义的习惯 + +*Examples*: [record_types](src/record_types.erl) + +```erlang +-module(record_types). + +-export([records/0]). + +-record(bad, {no_type}). + +-record(good, {with_type :: string(), with_value_type = 1 :: non_neg_integer()}). + +records() -> [#bad{}, #good{}]. +``` + +*原因*: 记录(`record`)定义的是数据结构, 而其中最重要的部分之一就是记录组成部分的类型定义. + +### 其它 + +*** +#### 给函数添加-spec函数规范定义 +*Examples*: [specs](src/specs.erl) + +*原因*: 1 便于Dialyzer分析 + 2 更容易知道函数的参数类型和返回以及用法 + +*** +#### 模块中不要用import +> Do not use the `-import` directive + +*Examples*: [import](src/import.erl) + +*原因*:从其他模块导入函数会使代码更难以读取和调试,因为您无法直接区分本地函数和外部函数。 +*** + +*** +#### Don't Use Case Catch +> 不要用`case catch` 捕获匹配异常, 使用 `try ... of ... catch` 代替 `case catch`. + +*Examples*: [case-catch](src/case_catch.erl) + +*原因*: `case catch ...` 把正确的的结果与异常一起处理令人困惑。 + `try ... of ... catch` 把正确的的结果与异常分开处理。 + +## 好的建议和方法 +当我们写代码时,应该考虑以下一些注意事项,但是不要引发PR拒绝,或者含糊到无法连贯执行。 + +*** +### 优先使用高级函数而不是手写的递归方法 +> 有时实现函数最好的方式是编写递归函数, 但是比较经常的写法是使用 fold函数 或者 列表推导式 会更加安全和可读性更高. + +*Examples*: [alternatives to recursion](src/recursion.erl) + +```erlang +-module(recursion). + +-export([recurse/1, fold/1, map/1, comprehension/1]). + +%% +%% 例子: +%% 不同的方法实现大写字符串 +%% + +%% 差的: 使用不必要的人工手写递归 +recurse(S) -> + lists:reverse(recurse(S, [])). + +recurse([], Acc) -> + Acc; +recurse([H | T], Acc) -> + NewAcc = [string:to_upper(H) | Acc], + recurse(T, NewAcc). + +%% 好的: 使用fold函数实现同样的结果,更加安全,更少的代码行数 +fold(S) -> + Result = lists:foldl(fun fold_fun/2, [], S), + lists:reverse(Result). + +fold_fun(C, Acc) -> + [string:to_upper(C) | Acc]. + +%% 更佳的: 使用map函数代替fold函数,更简单的实现方法,因为在这种情况下,fold函数大材小用了。 +map(S) -> + lists:map(fun string:to_upper/1, S). + +%% 最好的: 在这种情况下,列表推导式最简单的实现方法(假设忽略string:to_upper也能直接对string使用的事实) +comprehension(S) -> + [string:to_upper(C) || C <- S]. +``` + +*原因*: 人工手写的递归容易出错, 并且代价昂贵。在有错误的情况下,一个错误的递归函数会失去它的基本功能, +如螺旋般地失去控制,导致整个erlang节点挂掉,抵消了erlang最主要的好处之一: 进程的死亡不会导致整个节点的崩溃。 + +另外,对于一个有经验的erlang开发者而言,folds 和 列表推导式比复杂的递归函数更容易理解。 +显而易见的是:它们能为列表中的每个元素执行操作,递归也许同样能够实现,但是它经常需要仔细的检查,以验证控制流在实践中实际执行的路径。 + +*** +### 驼峰式命名,下划线命名 +> 符号命名:使用驼峰式命名变量,原子,函数和模块则使用下划线命名 +> *Examples*: [camel_case](src/camel_case.erl) +```erlang +-module(camel_case). + +-export([bad/0, good/0]). +%% 差的 +bad() -> + Variable_Name = moduleName:functionName(atomConstant), + another_ModuleName:another_Function_Name(Variable_Name). +%% 好的 +good() -> + VariableName = module_name:function_name(atom_constant), + another_module_name:another_function_name(VariableName). +``` + +*小节结论*:本节对下面一个问题很有帮助。 + +*** +### 更短(但仍保持有意义的)的变量名称 + +> 只要易于阅读和理解,保持变量名称简短。 + +*Examples*: [var_names](src/var_names.erl) +```erlang +-module(var_names). + +-export([bad/1, good/1]). +%% 差的 +bad(OrganizationToken) -> + OID = organization:get_id(OrganizationToken), + OID. +%% 好的 +good(OrgToken) -> + OrgID = organization:get_id(OrgToken), + OrgID. +``` + +*小节结论*: 它有助于减少每行的长度,这也是上面描述的。 + +*** +### 注释等级 + +> 模块注释用 **%%%**, 函数注释用 **%%**, 代码注释用 **%**. + +*Examples*: [comment_levels](src/comment_levels.erl) + +```erlang +% 这样的注释坏到家了 +%%% @doc 这样的注释不错 +-module(comment_levels). + +-export([bad/0, good/0]). + +% @doc 这样的注释不好 +%%% @doc 这的注释也不好 +bad() -> + R = 1 + 2, %%% 这样的注释不好(not good) + R. %% 这样的注释依然不好(bad again) + +%% @doc 这种注释我喜欢 +good() -> + % 这个注释得到国际注释协会的一致认可 + % 还有 Chuck Norris的认可 + R = 1 + 2, + R. % This comment (megusta) 这个注释我喜欢(megusta 西班牙语:我喜欢) +``` +*小节结论*: 清晰的陈述了注释是什么, 并且寻找特定的注释比如:"%% @"等 是非常有用的。 + 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。 + 修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。 + 注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。 + 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。 + +*** +### 保持函数精简 +> 只做一件事,尝试着用少量表达式来写函数. 除了集成测试外,每个函数理想的表达式数量是不超过**12**个. + +*Examples*: [small_funs](src/small_funs.erl) + +*小结*: 从3个方面: +- 简洁的函数有助于是可读性和组装性。可读性又有助于维护。这一点强调的足够多了,你的代码越简洁,就越容易修复和更改。 +- 简洁的函数目的清晰明了,因此您只需要了解执行操作的其中一小部分的子集,这使得验证它是否正确地工作变得非常简单。 + +- 强有力的论据: + + 一个函数只干一件事情,如果函数太冗长你可能更适合改为以多个函数实现 + + 很明显,简单的,简洁的函数更容易理解 + + 重用性,保持函数的精简有利于后续使用(特别是erlang) + + 屏幕尺寸:出于如何原因如果通过ssh连接服务器或者,你希望能够看到整个函数 + + +*提示*: + +本指导, 联合 **[避免多层嵌套](#avoid-deep-nesting)** and +**[在case表达式中使用更多更小的函数](#more-smaller-functions-over-case-expressions)** +两个指导, 可以很好的利用来构建函数,如下所示: + +```erlang +some_fun(#state{name=foo} = State) -> + do_foo_thing(), + continue_some_fun(State); +some_fun(#state{name=bar} = State) -> + do_bar_thing(), + continue_some_fun(State). + +continue_some_fun(State) -> + ..., + ok. +``` +记住这些: + +- 像这样在函数结尾调用函数是没有代价的 +- 这种模式高效、紧凑、清晰 +- 这样重置缩进,因此代码不会游离于屏幕右边边缘地带 +最重要的: +- 测试起来简单,因为函数描绘了测试节点. +- 提供更多的跟踪切入面,因此我们能够找到哪里的代码计算运行导致脱轨,而嵌套case写法在运行时是不可跟踪的。 + + +*** +### 避免不必要调用length/1 +> 许多用`length/1`作为`case`条件都可以被模式匹配替代掉,尤其在检查列表是否至少有一个元素时很管用。 +(要遍历列表,时间长度不定) +*Examples*: [pattern matching](src/pattern_matching.erl) +```erlang +-module(pattern_matching). + +-export([bad/1, good/1]). + +bad(L) -> + case length(L) of + 0 -> error; + _ -> ok + end. + +good([]) -> + error; +good(_L) -> + ok. +``` +*小结*:模式匹配是`Erlang`的核心内容之一,并且它的性能和可读性都很好。模式匹配也更加灵活,因此它使得代码逻辑变得更加简单。 + +防坑指南---------------------------------------------------------------------------------------- +'--' 运算与 '++'运算 + > [1,2,3,4] -- [1] -- [2]. + [2,3,4] +这是从后面算起的,先算 [1] -- [2] ,得到 [1] 后被 [1,2,3,4] --,最后得到 [2,3,4] + '++'运算也是一样的,也是从后面开始算起。 +> [1,2,3,4] -- [1] ++ [2,3,4]. + [] + +++只是lists:append/2的一个别名:如果要用一定要确定 ShortList++LongList + +erlang:list_to_binary() +如果参数是多层嵌套结构,就会被扁平化掉,使用 binary_to_list 不能转成原来的数据,也就是不可逆的。 +6> list_to_binary([1,2,[3,4],5]) . +<<1,2,3,4,5>> +如果想可逆,可以使用 erlang:term_to_binary +7> binary_to_term(term_to_binary([1,2,[3,4],5])). +[1,2,[3,4],5] + +ets:tablist/2在数据比较大的时候尽量少用 + +erlang 不同数据类型比较 +number < atom < reference < fun < port < pid < tuple < list < bit string + +Erlang中整数值没有上限值,最大值只是受限于硬件(内存有多大) 但是erlang的浮点数是有上限的遵循 IEEE754 + +在 guard 模式下,原本会抛出异常的代码就不会报错了,而是结束当前计算并返回false。 +在 guard 模式下,erlang是有两种表达方法的: +1. , 和 ; +2. andalso 和 orelse + +然而,这两种表述是有区别的: + +首先,假如条件语句是这样的 X >= N; N >= 0, + 当前半句出现异常时候,后面半句还是会执行,而且结果可能回是true; +然后,假如条件语句是这样的 X >= N orelse N >= 0, + 当前半句出现异常时候,后面半句是会被跳过的,返回的结果就是异常 +而其实两种都有各自的优缺点,所以,很多情况下都是把他们两种混合起来使用,达到业务需求 +最后补充下,when 之后是不允许使用自定义function的,会产生副作用,所以只能是跟整数比较或者是内部的函数,如:is_integer/1,is_atom/1. +顺带讲一下 and andalso or orelse +and 和 or 两边参与运算的必须是true或者是false才行 返回值也必然是true或者false 但是andalso orelse跟这个是短路求值 是有点差别的 比如下面 +(A > 1 orelse io:format("this is run ~n")). +(A > 1 andalso io:format("this is run ~n"). +(true andalso io:format("this is run ~n") andalso io:format("this is run2 ~n")). +(true andalso io:format("this is run ~n") andalso io:format("this is run2 ~n")). +true andalso false andalso fdfd . +true andalso false andalso io:format("this is run ~n") orelse io:format("this is run2 ~n"). +true andalso false andalso io:format("this is run ~n") andalso io:format("this is run2 ~n"). +true andalso false orelse io:format("this is run ~n") andalso io:format("this is run2 ~n"). + + + + +其他的更多的 +[那些经历过的Erlang小坑1-10](https://www.cnblogs.com/zhongwencool/p/3712909.html) +[那些经历过的Erlang小坑11-20](http://www.bubuko.com/infodetail-249770.html) +[那些经历过的Erlang小坑21-30](https://www.cnblogs.com/zhongwencool/p/erlang_tip_21_30.html) + + + diff --git a/src/docs/erlang进程.md b/src/docs/erlang进程.md new file mode 100644 index 0000000..966e969 --- /dev/null +++ b/src/docs/erlang进程.md @@ -0,0 +1,344 @@ +Erlang 进程相关学习 +==================================== + +# 目录 +1. [actor模型和进程特性](#actor模型和进程特性) +2. [进程创建](#进程创建) +3. [进程监控与注册](#进程注册与监控) +4. [进程调度](#进程调度) +5. [进程发送消息](#进程发送消息) +6. [进程接收消息](#进程接收消息) +7. [进程GC](#进程GC) +8. [更多](#更多) + + + +## actor模型和进程特性 + ### actor模型 + 在计算机科学中,它是一个并行计算的数学模型,最初为由大量独立的微处理器组成的高并行计算机所开发,Actor模型的理念非常简单: + 天下万物皆为Actor。Actor之间通过发送消息来通信,消息的传送是异步的,通过一个邮件队列(mail queue)来处理消息。每个Actor + 是完全独立的,可以同时执行它们的操作。每一个Actor是一个计算实体,映射接收到的消息到以下动作: + 1. 发送有限个消息给其它Actor + 2. 创建有限个新的Actor + 3. 为下一个接收的消息指定行为 + + 以上三种动作并没有固定的顺序,可以并发地执行。Actor会根据接收到的消息进行不同的处理。 + 简而言之: 一个Actor指的是一个最基本的计算单元,它能接收一个消息并且基于其执行计算。 + 综上,我们知道可以把系统中的所有事物都抽象成一个Actor,那么在一个系统中,可以将一个大规模的任务分解为一些小任务,这些小任务 + 可以由多个Actor并发处理,从而减少任务的完成时间和任务复杂度。 + 为什么会在讲Erlang进程的时候讲Actor模型的概念,就是因为对于Erlang的并发编程模型正是基于Actor模型,Erlang的代码运行在 + 进程中,而进程就是Erlang称呼Actor的方式,Eralng也是最著名的使用Actor规则的编程的语言。 + + ### Eralng进程特性 + 在Erlang的进程不是我们传统上的进程,Erlang进程是轻量级进程,它的生成、上下文切换和消息传递是由虚拟机管理的,操作系统 + 线程进程和Erlang进程之间没有任何联系,这使并发有关的操作不仅独立于底层的操作系统,而且也是非常高效和具有很强可扩展性。 + 它运行在 Erlang 虚拟机上,非常小,非常轻,可以瞬间创建上万,甚至几十万个,进程间完全是独立的内存空间执行,不共享内存, + 这些独立的内存空间可以独立的进行垃圾回收,基于独立运行,在发生错误的时候也是隔离的,其他不相关的进程可以继续运行。 + 在进程运行时若出现错误,由于进程的轻量级,Erlang 采取的措施是“任其崩溃”和“让其他进程修复”。 + 在Erlang上查看默认限制数量是26万多,可以进行修改。每个进程创建后都会有一个独一无二的 Pid,这些进程之间通过 Pid 来互相发 + 送消息,进程的唯一交互方式也是消息传递,消息也许能被对方收到,也许不能,收到后可以处理该消息。消息发送是异步的如果想知道某 + 个消息是否被进程收到,必须向该进程发送一个消息并等待回复。 + +## 进程创建 +Erlang 中的并发编程只需要如下几个简单的函数。 +```erlang +Pid = spawn(Mod,Func, Args) +``` +创建一个新的并发进程来执行Mod模块中的 Fun(),Args 是参数。 +跟上面提供的spawn/3功能相同的函数还有: +```erlang +spawn(Fun) -> pid() +spawn(Node, Fun) -> pid() +spawn(Module, Function, Args) -> pid() +spawn(Node, Module, Function, Args) -> pid() +spawn_link(Fun) -> pid() +spawn_link(Node, Fun) -> pid() +spawn_link(Module, Function, Args) -> pid() +spawn_link(Node, Module, Function, Args) -> pid() +spawn_monitor(Fun) -> {pid(), reference()} +spawn_monitor(Module, Function, Args) -> {pid(), reference()} +spawn_opt(Fun, Options) -> pid() | {pid(), reference()} +spawn_opt(Node, Fun, Options) -> pid() | {pid(), reference()} +spawn_opt(Module, Function, Args, Options) ->pid() | {pid(), reference()} +spawn_opt(Node, Module, Function, Args, Options) ->pid() | {pid(), reference()} +``` +创建好进程,返回对应的Pid之后向就可以向进程进程发送消息,erlang用 “!”来发送消息,格式如下。notice:消息发送是异步的, +发送方不等待而是继续之前的工作。 +```erlang +Pid !Message, +Pid1 ! Pid2 ! Pid3 ! Pid..n ! Message. +``` + +erlang用 receve ... end 来接受发送给某个进程的消息,匹配后处理,格式如下。 +```erlang +receive + Pattern1 [when Guard1] -> + Expression1; + Pattern2 [when Guard2] -> + Expression2; + ... + after T -> + ExpressionTimeout +end +``` +某个消息到达后,会先与 Pattern 进行匹配,匹配相同后执行,若未匹配成功消息则会保存起来待以后处理,进程会开始下一轮操作, +若等待超时T,则会执行表达式 ExpressionTimeout。 + + +## 进程注册与监控 + ### 进程注册 + 有些时候使用通过进程Pid来标识进程需要维护进程Pid,出于某些原因维护进程Pid,不方便灵活,比如你给某个服务器进程请求数据, + 你还得考虑怎么得到服务器进程的Pid,有些时候进程由于某种异常重启后Pid会发生变化,如果没有及时同步机制,会导致功能异常, + 于是乎Erlang提供了一套进程注册管理的机制----注册进程Erlang中管理注册进程的有4个内置函数,register、unregister、 + whereis、registered,它们的用法如下: + 1)register(Atom, Pid):将一个进程Pid注册一个名为AnAtom的原子,如果原子AnAtom已经被另一个注册进程所使用, + 那么注册就会失败。 + 2)unregister(Atom):移除与AnAtom相对应进程的所有注册信息。如果一个注册死亡,那么它也会被自动取消注册。 + 3)whereis(Atom) -> Pid | undefined:判断AnAtom是否已经被其他进程注册。如果成功,则返回进程标识符Pid。 + 如果AnAtom没有与之相对应的进程,那么就返回原子undefined。 + 4)registered() -> [AnAtom ::atom()]:返回一个系统中所有已经注册的名称列表。 + + ### 进程监控 + Erlang 对于进程处理理念之一是“任其崩溃”和“让其他进程修复”,常规Erlang系统中有很多进程同时运行,进程之间可能相互依赖, + 这么复杂的情况之下怎么实现该理念呢?Erlang除了提供exception,try catch等语法,还支持Link和Monitor两种监控进程的机制, + 使得所有进程可以连接起来,组成一个整体。当某个进程出错退出时,其他进程都会收到该进程退出的消息通知。有了这些特点,使用erlang + 建立一个简单,并且健壮的系统就不是什么难事。 + #### 进程双向监控-Link + 相关API link(Pid), A进程调用了link(Pid) 则A进程与Pid之间就建立起了双向连接,如果两个进程相连接,如果其中一个终止时, + 讲发送exit信号给另一方,使其终止,同时终止进程会依次发送exit信号给所有与其连接的进程,这使得exit信号在系统内层层蔓延。 + 该函数连接不存在的进程时会导致发起连接的进程终止 + spawn_link()系列函数 它与link(Pid)的差别就是 原子性与非原子性 + unlink(Pid) 移除调用进程与Pid的连接 + 通过调用process_flag(trap_exit, true)可以设置捕捉exit信号, + 假如有A,B两个进程且彼此link + 总结... + 1.当A的结束原因是normal时(进程正常执行完就是normal),B是不会退出的,此时link机制不发生作用 + 2.若A的结束原因是killed,例如调用exit(PidA,kill) ,则无论B是否有设置trap_exit,B都会terminate,此时退出信号捕捉机制是无效的 + 3.若A的结束原因不是normal也不是killed(例如exit(PidA,Reason)),那么B在设置了trap_exit时,会捕捉到退出信号, + 取而代之的是收取到一条消息{‘EXIT’,Pid,Reason},这时B不会结束,用户可以根据收到的消息对A进程的结束进行处理;若B没有设置trap_exit,B就会terminate + + |捕获状态 |退出信号(原因) |动作 | + | :-------------------| ------------------: | :--------------------------------------:| + |false | normal | 不做任何事 | + |false | kill | 消亡,向链接的进程广播退出信号(killed) | + |false | X | 消亡,向链接的进程广播退出信号X | + |true | normal | 接收到{'EXIT', Pid, nomal} | + |true | kill | 消亡,向链接的进程广播退出信号(killed) | + |true | X | 将{'EXIT', Pid, X} 加入到邮箱 | + #### 监视器(monitor) + 相关API + monitor(process, monitor_process_identifier()) %monitor_process_identifier() 为Pid或者已注册的进程名称 + demonitor(MonitorRef) + demonitor(MonitorRef, OptionList) + 监视器与link不同的是它是单向式观察一些进程终止,各个监视器通过Erlang的引用相互区分,是调用monitor返回的,具有唯一性, + 而且A进程可以设置多个对B进程的监视器,每一个通过不同的引用区分。 + 当被监视的进程终止时,一条格式{'Down',Reference, process, Pid, Reason}的消息会被发给监视此进程的进程 + 调用erlang:demonitor(Reference)可以移除监视器, + 调用erlang:demonitor(Reference,[flush])可以让该监视进程邮箱中所有与Reference对应的{'DOWN', Reference,process,Pid,Reason} + 的消息被冲刷掉。 + 如果尝试监视一个不存在的进程会导致收到一条{'DOWN', process, Pid,Reason}的消息,其中Reason为noproc,这和link()不一样 + +## 进程调度 +就目前计算机体系结构而言,任何进程或线程要执行就需要得到CPU资源,对于erlang的进程同样如此。erlang虚拟机同时存在成千上万的进程, +但是cpu核心数又是有限的,所有erlang并发特性就需要一个合适的调度规则来安排各个进程的运行, +简单而言,erlang虚拟机调度程序保留两个队列,准备好运行的就绪队列以及等待接收消息的进程的等待队列。当等待队列中的进程收到消息或获 +得超时时,它将被移动到就绪队列。调度程序从就绪队列中选择第一个进程并将其交给BEAM执行一个时间片。当时间片用完时,BEAM会抢占正在 +运行的进程,并将进程添加到就绪队列的末尾。如果在时间片用完之前在接收中阻止了进程,则会将其添加到等待队列中。 + +Erlang调度器主要有以下特点: +1. 进程调度运行在用户空间 :Erlang进程不同于操作系统进程,Erlang的进程调度也跟操作系统完全没有关系,是由Erlang虚拟机来完成的; +2. 调度是抢占式的:每一个进程在创建时,都会分配一个固定数目的reduction(这个数量默认值是2000),每一次操作(函数调用), + reduction就会减少,当这个数量减少到0时或者进程没有匹配的消息时,抢占就会发生(无视优先级); +3. 每个进程公平的使用CPU:每个进程分配相同数量的reduction,可以保证进程可以公平的(不是相等的)使用CPU资源 +4. 调度器保证软实时性:Erlang中的进程有优先级,调度器可以保证在下一次调度发生时,高优先级的进程可以优先得到执行。 + +Reduction +受操作系统中基于时间片调度算法的影响,一开始知道有reduction这个概念时,一直想搞清楚这个reduction到底对应多长的绝对时间,不过, +从Erlang本身对reduction的使用来看,完全没有必要纠结这个问题。《Erlang编程指南》一书中对reduction的说明如下: +程序中的每一个命令,无论它是一个函数调用,还是一个算术操作,或者内置函数,都会分配一定数量的reduction。虚拟机使用这个值来衡量一个 +进程的活动水平。 + +进程优先级 +Erlang进程有四种优先级:max, high, normal, low(max只在Erlang运行时系统内部使用,普通进程不能使用)。Erlang运行时有两个 +运行队列对应着max和high优先级的运行任务,normal和low在同一个队列中。调度器在调度发生时,总是首先查看具体max优先级的进程队列, +如果队列中有可以进行的进程,就会运行,直到这个队列为空。然后会对high优先级的进程队列做同样的操作(在SMP环境,因为同时有几个调度器,所以在同一时间,可能会有不同优先级的任务在同时运行; +但在同一个调度器中,同一时间,肯定是高优先级的任务优先运行)。普通进程在创建时,一般是normal优先级。normal和low优先级的进程只有 +在系统中没有max和high优先级的进程可运行时才会被调度到。通常情况下,normal和low优先级的进程交替执行,low优先级获得CPU资源相对 +更少(一般情况下):low优先级的任务只有在运行了normal优先级任务特定次数后(在R15B中,这个数字是8)才会被调度到(也就是说只有 +在调度了8个normal优先级的进程后,low优先级的进程才会被调度到,即使low优先级的进程比normal优先级的进程更早进入调度队列,这种 +机制可能会引起优先级反转:假如你有成千上万的活动normal进程,而只有几个low优先级进程,那么相比normal进程,low优先级可能会获得 +更多的CPU资源)。 + +## 进程发送消息 +Erlang系统中,进程之间的通信是通过消息传递来完成的。消息使用Pid ! Message的形式发送,通过receive语句获取。每个Erlang进程 +都有用来存储传入消息的信箱。当一个消息发送的时候,它会从发送进程中拷贝到接收进程的信箱,并以它们到达的时间次序存储。消息的传递是 +异步的,一个发送进程不会在发送消息后被暂停。 + +上面提到发送消息时,会在两个进程之间存在消息复制,为什么需要复制呢?这就跟进程的堆内存有关。虽然在Erlang的文档(heap_type)中 +说明堆内存有三种类型:private,shared,hybrid,但是在实际的代码中,只有两种private和hybrid +(参见[$R15B_OTP_SRC/erts/emulator/beam/erl_bif_info.c --> system_info_1]), +(参见[$R15B_OTP_SRC/erts/Makefile.in:# Until hybrid is nofrag, don't build it.), +也就是说Erlang目前的堆内存只有一种:private。 +private类型的堆内存是跟shared类型相对的:shared是指所有线程共享同一块内存(比如Java),多个线程对同一块内存的访问需要锁保护; +而private类型的堆内存是指每个进程独享一块内存,对于内存的访问不需要锁保护。 +在Erlang的private堆内存架构下,发送消息需要做三件事件: + 1. 计算消息的大小,并在接收进程的内存空间中给消息分配内存; + 2. 将消息的内容拷贝到接收进程的堆内存中; + 3. 最后将消息的地址添加到接收进程的消息队列。 +从上面的步骤可以看出,拷贝消息的代码是O(n),n是消息的长度,也就是说消息越长,花费越大。所以在使用Erlang时,要避免大数据量的大消息传递。 + +在shared堆内存架构下,发送消息只需要O(1)(只传递消息地址),那为什么Erlang要默认选择private类型的堆内存呢? +其实这跟后面要讲到的Erlang的GC相关:private的优势就是GC的延迟很低,可以很快的完成(因为只保存一个进程的数据, +GC扫描时的数据量很小)。在SMP环境下,实际上每个进程有两个消息队列。进程发送消息时,实际上消息是添加到目标进程的公有队列 +(通过锁来保证互斥访问);而目标进程在消费消息时,实际上是在自己的私有消息队列上处理的,从而减小锁带来的访问开销。但是, +如果目标进程在自己的私有消息队列上无法匹配到消息,那么公有队列中的消息将被添加到私有队列。 + +## 进程接收消息 +```erlang +receive + Pattern1 [when Guard1] -> + Expression1; + Pattern2 [when Guard2] -> + Expression2; + ... + after T -> + ExpressionTimeout +end +``` +整个过程如下 + 1. 当我们输入receive语句时,我们启动一个计时器(如果有after T)。 + 2. 获取邮箱中的第一个消息,并尝试将其与Pattern1、Pattern2等进行匹配。 + 如果匹配成功,则从邮箱中删除消息,并计算模式后面的表达式。 + 3. 如果receive语句中的任何模式都不匹配邮箱中的第一个消息,那么第一个消息将从邮箱中删除并放入“save队列”中。 + 然后尝试邮箱中的第二条消息。重复此过程,直到找到匹配的消息或检查邮箱中的所有消息为止。 + 4. 如果邮箱中的所有消息都不匹配,则进程将被挂起,并在下次将新消息放入邮箱时重新安排执行时间。注意,当新消息到达时, + 保存队列中的消息不会重新匹配;只匹配新消息( Erlang的实现是非常“聪明”的,并且能够最小化每个消息被接收方的receive测试的次数) + 5. 一旦匹配了消息,那么所有放入save队列的消息都将按照到达进程的顺序重新进入邮箱。如果设置了计时器, + 则清除计时器。 + 6. 如果计时器在等待消息时超时,则计算表达式ExpressionsTimeout,并按到达进程的顺序将任何保存的消息放回邮箱。 + +## 进程GC +erlang 进程GC +Memory Layout 内存分布 + +在我们深入垃圾回收机制之前,我们先来看看Erlang进程的内存布局. 一个Erlang进程的内存布局通常分为是三个部分(有人认为是四个部分, +把mailbox作为单独的一个部分), 进程控制块, 堆和栈,和普通的Linux进程的内存布局非常类似. +``` + Shared Heap Erlang Process Memory Layout + + +----------------------------------+ +----------------------------------+ + | | | | + | | | PID / Status / Registered Name | Process + | | | | Control + | | | Initial Call / Current Call +----> Block + | | | | (PCB) + | | | Mailbox Pointers | + | | | | + | | +----------------------------------+ + | | | | + | | | Function Parameters | + | | | | Process + | | | Return Addresses +----> Stack + | | | | + | +--------------+ | | Local Variables | + | | | | | | + | | +------------+--+ | +-------------------------------+--+ + | | | | | | | | + | | | +-------------+--+ | | ^ v +----> Free + | | | | | | | | | Space + | | | | +--------------+-+ | +--+-------------------------------+ + | +-+ | | | | | | + | +-+ | Refc Binary | | | Mailbox Messages (Linked List) | + | +-+ | | |   | + | +------^---------+ | | Compound Terms (List, Tuples) | Process + | | | | +----> Private + | | | | Terms Larger than a word | Heap + | | | | | + | +--+ ProcBin +-------------+ Pointers to Large Binaries | + | | | | + +----------------------------------+ +----------------------------------+ +``` +进程控制块: 进程控制块持有关于进程的一些信息, 比如PID, 进程状态(running, waitting), 进程注册名, 初始和当前调用, +指向进程mailbox的指针 + +栈: 栈是向下增长的, 栈持有函数调用参数,函数返回地址,本地变量以及一些临时空间用来计算表达式. + +堆: 堆是向上增长的, 堆持有进程的mailbox, 复合terms(Lists, Tuples, Binaries),以及大于一个机器字的对象(比如浮点数对象). +大于64个字节的二进制terms,被称为Reference Counted Binary, 他们不是存在进程私有堆里面,他们是存在一个大的共享堆里,所有进程 +都可以通过指向RefC Binary的指针来访问该共享堆,RefC Binary指针本身是存在进程私有堆里面的. + +GC Details +为了更准确的解释默认的Erlang垃圾回收机制, 实际上运行在每个独立Erlang进程内部的是分代拷贝垃圾回收机制, 还有一个引用计数的 +垃圾回收运行在共享堆上. + +Private Heap GC 私有堆垃圾回收 +私有堆的垃圾回收是分代的. 分代机制把进程的堆内存分为两个部分,年轻代和年老代. 区分是基于这样一个考虑, 如果一个对象在运行 +一次垃圾回收之后没有被回收,那么这个对象短期内被回收的可能性就很低. 所以, 年轻代就用来存储新分配的数据,年老代就用来存放运行 +一定次数的垃圾回收之后依然幸存的数据. 这样的区分可以帮助GC减少对那些很可能还不是垃圾的数据不必要的扫描. 对应于此, Erlang的 +GC扫描有两个策略, Generational(Minor) 和 Fullsweep(Major). Generational GC只回收年轻代的区域, 而Fullsweep则同时回收年轻代和 +年老代. + +下面我们一起来review一下一个新创建的Erlang进程触发GC的步骤, 假设以下不同的场景: + +场景 1: + +Spawn > No GC > Terminate +假设一个生存期较短的进程, 在存活期间使用的堆内存也没有超过 min_heap_size,那么在进程结束是全部内存即被回收. + +场景 2: + +Spawn > Fullsweep > Generational > Terminate +假设一个新创建的进程,当进程的数据增长超过了min_heap_size时, fullsweep GC即被触发, 因为在此之前还没有任何GC被触发,所以堆区 +还没有被分成年轻代和年老代. 在第一次fullsweep GC结束以后, 堆区就会被分为年轻代和年老代了, 从这个时候起, GC的策略就被切换为 +generational GC了, 直到进程结束. + +场景 3: + +Spawn > Fullsweep > Generational > Fullsweep > Generational > ... > Terminate +在某些情景下, GC策略会从generation再切换回fullsweep. 一种情景是, 在运行了一定次数(fullsweep_after)的genereration GC之后, +系统会再次切换回fullsweep. 这个参数fullsweep_after可以是全局的也可以是单进程的. 全局的值可以通过函数erlang:system_info(fullsweep_after)获取, +进程的可以通过函数erlang:process_info(self(),garbage_collection)来获取. 另外一种情景是, 当generation GC(minor GC)不能够收集到足够的内存空间时. +最后一种情况是, 当手动调用函数garbage_collector(PID)时. 在运行fullsweep之后, GC策略再次切换回generation GC直到以上的任意一个情景再次出现. + +场景 4: + +Spawn > Fullsweep > Generational > Fullsweep > Increase Heap > Fullsweep > ... > Terminate +假设在场景3里面,第二个fullsweep GC依然没有回收到足够的内存, 那么系统就会为进程增加堆内存, 然后该进程就回到第一个场景,像刚创建的进程一样首先 +开始一个fullsweep,然后循环往复. + +那么对Erlang来说, 既然这些垃圾回收机制都是自动完成的, 为什么我们需要花时间去了解学习呢? 首先, 通过调整GC的策略可以使你的系统运行的更快. 其次, + 了解GC可以帮助我们从GC的角度来理解为什么Erlang是一个软实时的系统平台. 因为每个进程有自己的私有内存空间和私有GC,所以每次GC发生的时候只在进程 + 内部进行,只stop本进程, 不会stop其他进程,这正是一个软实时系统所需要的. + +Shared Heap GC 共享堆垃圾回收 +共享堆的GC是通过引用计数来实现的. 共享堆里面的每个对象都有一个引用计数,这个计数就是表示该对象被多少个Erlang进程持有(对象的指针存在进程的私有堆里). + 如果一个对象的引入计数变成0的时候就表示该对象不可访问可以被回收了. + + 进程调度 + ## ## 进程调度 +就目前计算机体系结构而言,任何进程或线程要执行就需要得到CPU资源,对于erlang的进程同样如此。erlang虚拟机同时存在成千上万的进程, +但是cpu核心数又是有限的,所有erlang并发特性就需要一个合适的调度规则来安排各个进程的运行, +简单而言,erlang虚拟机调度程序保留两个队列,准备好运行的就绪队列以及等待接收消息的进程的等待队列。当等待队列中的进程收到消息或获 +得超时时,它将被移动到就绪队列。调度程序从就绪队列中选择第一个进程并将其交给BEAM执行一个时间片。当时间片用完时,BEAM会抢占正在 +运行的进程,并将进程添加到就绪队列的末尾。如果在时间片用完之前在接收中阻止了进程,则会将其添加到等待队列中。 + +Erlang调度器主要有以下特点: +1. 进程调度运行在用户空间 :Erlang进程不同于操作系统进程,Erlang的进程调度也跟操作系统完全没有关系,是由Erlang虚拟机来完成的; +2. 调度是抢占式的:每一个进程在创建时,都会分配一个固定数目的reduction(这个数量默认值是2000),每一次操作(函数调用), + reduction就会减少,当这个数量减少到0时或者进程没有匹配的消息时,抢占就会发生(无视优先级); +3. 每个进程公平的使用CPU:每个进程分配相同数量的reduction,可以保证进程可以公平的(不是相等的)使用CPU资源 +4. 调度器保证软实时性:Erlang中的进程有优先级,调度器可以保证在下一次调度发生时,高优先级的进程可以优先得到执行。 + +1. What operators does Erlang have? +``` +Arithmetic operators: + - * / div rem +Comparison operators: =:= == =/= /= > >= < =< +Logical operators: and andalso or orelse +Bitwise operators: bsl bsr Bitwise logical operators: band Bor bxor bnot +``` +--------------------- + + + + +