Erlang并发编程总结

并发编程

使用并发

spawn创建一个并行进程, send向某个进程发送消息, receive则是接收消息

Erlang的并发是基于进程( process)的。进程是一些独立的小型虚拟机,可以执行Erlang函数 ,在Erlang里,进程隶属于编程语言,而非操作系统。

Erlang中的进程会执行某个特点方法,并且执行过程是以一个并发的形式执行的

spawn(Mod, Func, Args) -> Pid,进程标识符 ,以用Pid来给此进程发送消息

可以使用 Pid ~Message 的形式给进程Pid发送消息

使用receive … of 来接收消息

rpc(Pid, Request) ->
    Pid ! {self(), Request},
    receive
	{Pid, Response} ->
	    Response
    end.


loop() ->
    receive
	{From, {rectangle, Width, Ht}} -> 
	    From ! {self(), Width * Ht},
	    loop();
	{From, {circle, R}} -> 
	    From !  {self(), 3.14159 * R * R},
	    loop();
	{From, Other} ->
	    From ! {self(), {error,Other}},
	    loop()
    end.

self()是客户端进程的标识符 ,可以用来获取当前进程的Pid

发送请求的进程通常称为客户端。接收请求并回复客户端的进程称为服务器。

睡眠Sleep

编写一个只有超时部分的receive。 通过这种方法,我们可以定义一个sleep(T)函数,它会让当前的进程挂起T毫秒

sleep(T) ->
   receive
   after T -> 
           ok
   end.

如果接收语句里的超时值是原子infinity(无穷大),就永远不会触发超时

定时器

可以用接收超时来实现一个简单的定时器
函数stimer:start(Time, Fun)会在Time毫秒之后执行Fun(一个不带参数的函数)。它返回一个句柄(是一个PID),可以在需要时用来关闭定时器

-module(stimer).
-export([start/2, cancel/1]).

start(Time, Fun) -> spawn(fun() -> timer(Time, Fun) end).

cancel(Pid) -> Pid ! cancel.

timer(Time, Fun) ->
    receive
	cancel ->
	    void
    after Time ->
	    Fun()
    end.

注册进程

Erlang有一种公布进程标识符的方法,它让系统里的任何进程都能与该进程通信。这样的进
程被称为注册进程( registered process)。

register(AnAtom, Pid)

用AnAtom( 一个原子)作为名称来注册进程Pid。如果AnAtom已被用于注册某个进程,这次注册就会失败

register(demo, spawn(?MODULE, loop,[])),
demo ! {msg}.
    

使用了注册进程之后,可以直接用注册进程原子名称进行发送消息,可以不需要Pid标识符

unregister(AnAtom)

移除与AnAtom关联的所有注册信息

whereis(AnAtom)

检查AnAtom是否已被注册。如果是就返回进程标识符Pid, 如果没有找到与AnAtom关联的进程就返回原子undefined。

registered() ->[AnAtom::atom()]

返回一个包含系统里注册进程的列表,注意是列表

分布式

在分布式Erlang里,我们编写的程序会在Erlang的节点( node)上运行。 节点包含自带地址空间和进程组的完整虚拟机

%% 启动某个节点
erl -sname 节点名称

接口:

在一个节点上调用执行一个函数

rpc:call(Node, Module, Function, Args) -> Res | {badrpc, Reason}

Node:节点的地址,如节点名称demo,则 demo@localhost

Module, Function, Args就跟apply的(Module, Function, Args)类似

通过rpc:call就能够远程调用服务

为了让两台不同网络的节点能够互通,因此需要在启动到时候设置相同的cookies

erl -sname test1 -setcookie abc

erl -sname test2 -setcookie abc

cookie系统让访问单个或一组节点变得更安全。每个节点都有一个cookie,如果它想与其他任何节点通信,它的cookie就必须和对方节点的cookie相同

cookie从不会在网络中明文传输,它只用来对某次会话进行初始认证。分布式Erlang会话不是加密的,但可以被设置成在加密通道中运行

套间字编程

gen_tcp用于编写TCP应用程序, gen_udp用于编写UDP应用程序

TCP

gen_tcp:connect

connect(Address, Port, Options, Timeout) 连接一个 TCP 端口

返回: {ok, Socket} | {error, Reason}

用给出的端口 Port 和 IP 地址 Address 连接到一个服务器上的 TCP 端口上。参数 Address 即可以是一个主机名,也可以是一个 IP 地址。参数 Timeout 指定一个以毫秒为单位的超时值,默认值是 infinity。

连接localhost:8888 端口

{ok, Socket} = gen_tcp:connect("localhost", 8888, [binary, {packet, 4}]),

连接调用里的binary参数告诉系统要以“二进制”模式打开套接字,并把所有数据用二进制型传给应用程序。

{packet,4}的意思是把未经修改的TCP数据直接传给应用程序,即每个逻辑请求或响应前面都会有一个4字节的长度计数,这个数字是客户端和服务端双方约定的

packet这个词在这里指的是应用程序请求或响应消息的长度,而不是网络上的实际数据包

客户端和服务器使用的packet参数必须一致。如果启动服务器时用了{packet,2},客户端用了{packet,4}, 程序就会失败

gen_tcp:send

在一个套接字 Socket 发送一个数据包

用法:

send(Socket, Packet) -> ok | {error, Reason}

代码:

Packet是请求的Request,要转为二进制型来操作

ok = gen_tcp:send(Socket, term_to_binary({Pid = self(), login})),
receive

在gen_tcp:send之后,需要{tcp,Socket,Bin} 来接收消息

Bin是二进制型,需要手动调用方法binary_to_term转为tuple

loop(Socket) ->
	receive
		{tcp, Socket, Bin} ->
			case binary_to_term(Bin) of
				{Pid, login} -> login(Socket, Pid);
				{Pid, logout} -> logout(Socket, Pid)
			end;
		{tcp_closed, Socket} ->
			stop
	end.

收到一个{tcp_closed, Socket}消息。这会在服务器完成数据发送时发生,意味着连接已经关闭

gen_tcp:listen

开启一个监听某个端口的套接字,在本地开启一个监听某个端口的套接字(socket)。开启成功的话,会返回一个套接字标识符 Listen,其一般会传递给 get_tcp:accept/1 或 get_tcp:accept/2 调用。

用法:

listen(Port, Options) -> {ok, Listen} | {error, Reason}

参数 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}:允许本地重复使用端口号
  • {nodelay, true}:意味着很少的数据也会被马上被发送出去
  • {delay_send, true}:数据不是立即发送,而是存到发送队列里,等 socket 可写的时候再发送
  • {backlog, 1024}: 缓冲待处理连接队列的最大长度,默认为5
  • {exit_on_close, false}:设置为 flase,那么 socket 被关闭之后还能将缓冲区中的数据发送出去
  • {send_timeout, 15000}:设置一个时间去等待操作系统发送数据,如果底层在这个时间段后还没发出数据,那么就会返回 {error,timeout}

Options是一个列表,里边存放以上参数,多个参数用逗号隔开,如:

{ok,Listen} = gen_tcp(8080, [ binary, {packet, 4}, {reuseaddr, true}, {active, true} ]).

gen_tcp:accept

接受一个套接字 ListenSocket 上的连接请求, ListenSocket 必须是由函数 gen_tcp:listen/2 建立返回

该函数会引起进程阻塞,直到有一个连接请求发送到监听的套接字。

用法:

accept(ListenSocket) -> {ok, Socket} | {error, Reason}

如果是使用了{active, true},则接收的方式:

loop(Socket) ->
	receive
		{tcp, Socket, Bin} ->
			io:format("Bin:~p~n", [Bin]),
			case binary_to_term(Bin) of
				{rectangle, Width, Height} -> gen_tcp:send(Socket, term_to_binary(Width * Height));
				{square, Side} -> gen_tcp:send(Socket, term_to_binary(Side * Side))
			end,
			loop(Socket);
		{tcp_closed, _Socket} ->
			io:format("server close ~n")
	end.

如果是使用了{active,false},则使用recv的形式接收数据:

loop(Socket) ->
	case gen_tcp:recv(Socket, 0) of
		{ok, Bin} ->
			io:format("Bin:~p~n", [Bin]),
			case binary_to_term(Bin) of
				{rectangle, Width, Height} -> gen_tcp:send(Socket, term_to_binary(Width * Height));
				{square, Side} -> gen_tcp:send(Socket, term_to_binary(Side * Side))
			end,
			loop(Socket);
		{error, closed} ->
			io:format("server close ~n")
	end.

gen_tcp:recv:从一个被动模式的套接字接受一个数据包

用法:

recv(Socket, Length) -> {ok, Packet} | {error, Reason}

这个函数是从一个被动模式的套接字接受一个数据包。如果返回一个 {error, closed} 的返回值,那表明 Socket 已经关闭

如果使用了{active, once}的形式,就必须显式调用inet:setopts才能重启下一个消息的接收,在此之前系统会处于阻塞状态

loop(Socket) ->
	receive
		{tcp, Socket, Bin} ->
			io:format("Bin:~p~n", [Bin]),
			case binary_to_term(Bin) of
				{rectangle, Width, Height} -> gen_tcp:send(Socket, term_to_binary(Width * Height));
				{square, Side} -> gen_tcp:send(Socket, term_to_binary(Side * Side))
			end,
			inet:setopts(Socket, [{active, once}]),
			loop(Socket);
		{tcp_close, Socket} ->
			io:format("server close ~n")
	end.
inet:peername

返回另一端连接的地址和端口

用法:

peername(Socket) -> {ok, {Address, Port}} | {error, posix()}

返回Socket对应的IP

{ok, {Ip, _Port}} = inet:peername(Socket),
inet:port

返回一个套接字的本地端口号

用法:

port(Socket) -> {ok, Port} | {error, any()}

案例
-module(socket_examples).
-compile(export_all).
-import(lists, [reverse/1]).


nano_get_url() ->
    nano_get_url("www.google.com").

nano_get_url(Host) ->
    {ok,Socket} = gen_tcp:connect(Host,80,[binary, {packet, 0}]), %% (1)
    ok = gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"),  %% (2)
    receive_data(Socket, []).

receive_data(Socket, SoFar) ->
    receive
	{tcp,Socket,Bin} ->    %% (3)
	    receive_data(Socket, [Bin|SoFar]);
	{tcp_closed,Socket} -> %% (4)
	    list_to_binary(reverse(SoFar)) %% (5)
    end.



nano_client_eval(Str) ->
    {ok, Socket} = 
	gen_tcp:connect("localhost", 2345,
			[binary, {packet, 4}]),
    ok = gen_tcp:send(Socket, term_to_binary(Str)),
    receive
	{tcp,Socket,Bin} ->
	    io:format("Client received binary = ~p~n",[Bin]),
	    Val = binary_to_term(Bin),
	    io:format("Client result = ~p~n",[Val]),
	    gen_tcp:close(Socket)
    end.



start_nano_server() ->
    {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4},  %% (6)
					 {reuseaddr, true},
					 {active, true}]),
    {ok, Socket} = gen_tcp:accept(Listen),  %% (7)
    gen_tcp:close(Listen),  %% (8)
    loop(Socket).

loop(Socket) ->
    receive
	{tcp, Socket, Bin} ->
	    io:format("Server received binary = ~p~n",[Bin]),
	    Str = binary_to_term(Bin),  %% (9)
	    io:format("Server (unpacked)  ~p~n",[Str]),
	    Reply = lib_misc:string2value(Str),  %% (10)
	    io:format("Server replying = ~p~n",[Reply]),
	    gen_tcp:send(Socket, term_to_binary(Reply)),  %% (11)
	    loop(Socket);
	{tcp_closed, Socket} ->
	    io:format("Server socket closed~n")
    end.



error_test() ->
    spawn(fun() -> error_test_server() end),
    lib_misc:sleep(2000),
    {ok,Socket} = gen_tcp:connect("localhost",4321,[binary, {packet, 2}]),
    io:format("connected to:~p~n",[Socket]),
    gen_tcp:send(Socket, <<"123">>),
    receive
	Any ->
	    io:format("Any=~p~n",[Any])
    end.

error_test_server() ->
    {ok, Listen} = gen_tcp:listen(4321, [binary,{packet,2}]),
    {ok, Socket} = gen_tcp:accept(Listen),
    error_test_server_loop(Socket).

error_test_server_loop(Socket) ->
    receive
	{tcp, Socket, Data} ->
	    io:format("received:~p~n",[Data]),
	    atom_to_list(Data),
	    error_test_server_loop(Socket)
    end.

创建某个套接字(通过调用gen_tcp:accept或gen_tcp:connect) 的进程被称为该套接字的控制进程。所有来自套接字的消息都会被发送到控制进程。如果控制进程挂了,套接字就会被关 闭 。

ETS和DETS

ets和dets是两个系统模块,可以用来高效存储海量的Erlang数据

ETS:可以用它存储海量的数据(只要有足够的内存),执行查找的时
间也是恒定的

ETS或DETS表其实就是Erlang元组的**集合 ,**ETS表没有垃圾收集机制

ETS表里的数据保存在内存里,它们是易失的。当ETS表被丢弃或者控制它的Erlang进程终止时,这些数据就会被删除。保存在DETS表里的数据是非易失的,即使整个系统崩溃也能留存下来。 DETS表在打开时会进行一致性检查,如果发现有损坏,系统就会尝试修复它

ETS和DETS表保存的是元组

一些表被称为异键表( set), 它们要求表里所有的键都是唯一的。另一些被称为同键表( bag),它们允许多个元素拥有相同的键

表类型标识说明
异键表set它们要求表里所有的键都是唯一的
有序异键ordered set与set类似,会对元组进行排序
异键bag不同的元组可以有相同的键
副本同键duplicate bag可以有多个元组拥有相同的键

ETS常用API

ets:new/2 创建一个 ets 表

用法:

ets:new(Name, Options) -> tid() | atom()

Name -> atom, ets表名称

Options -> [] ,可以写多个参数

Options:Type | Access | named_table | {keypos, Pos} | {heir, Pid :: pid(), HeirData} | {heir, none} |

named_table:标识了该参数,就能够直接使用Name来操作ets表

keypos是步长,integer() >= 1,用于指定元组第几位,常用在使用Record的时候

Type: set、ordered set、bag、duplicate bag

Access: 访问权限,分为public(任何进程可读写),private(除了创建进程,其他进程不可以读写),protected(默认)(主进程可读写,其他进程只读不可写)

举例:

%% 创建一个名叫test的set表,并且要使用test作为后续操作
TableId = ets:new(test,[set,named_table,public]).

ets:insert/2 向 ETS 表插入数据

用法:

ets:insert(Tab, ObjectOrObjects) -> true

  • 如果是 set 类型的表,并且在表里有跟插入的对象数据有相同的键,那么旧的对象数据将会被替换。
  • 如果是一个bag表,一个键可以有多个不同的值
  • 如果是一个duplicate bag,不同的值可以有相同的键

代码:

%% 在名为test的表上插入{a,1}数据
ets:insert(test, {a, 1}).
%% 批量插入
ets:insert(test,[{a,1},{b,2}]).

ets:lookup/2 在 ETS 表里查出相应键的值

用法:

ets:lookup(Tab, Key) -> [Object].

代码:

%% 查找test表中键为a的值
3> ets:lookup(test,a).
[{a,1}]

注意set返回的是一个[ {Key,Value} ]的形式,空则返回[]

而bag或duplicate_bag 则返回任意长度的列表

ets:lookup_element/3 返回 ETS 表里指定键的对象数据的第几个元素数据

用法:

ets:lookup_element(Tab, Key, Pos) -> Elem

Pos是步长

代码:

1> ets:new(test,[set,named_table]).
test
2> ets:insert(test,{a,1}).
true
3> ets:lookup_element(test,a,2).
1

注意,返回的是一个元素Elem

如果表里没有键为 Key 的对象,函数将以 badarg 的原因退出。

此时在代码中,可以使用 catch 的形式来捕获 没有Key的情况

case (catch ets:lookup_element(State, Ip, 2)) of
								  %% 如果为空就直接插入数据
								  {'EXIT', _} -> do_sthome...
									_Other ->do....
											  end.
        						

ets:insert_new/2 向 ETS 表插入新数据

用法:

insert_new(Tab, ObjectOrObjects) -> boolean()

这个函数跟 ets:insert/2 很相似,都是向 ETS 表 Tab 插入数据 ObjectOrObjects,不同的是,在插入数据时有相同的键要被覆盖替换(类型是 set 或 ordered_set 的表),或者要添加的多个对象数据的键已在表里存在(类型是 bag 或 duplicate_bag 的表),该函数则返回false。

如果插入的对象数据 ObjectOrObjects 是一个列表,那么在插入数据之前,将检测列表里的每一个键。如果列表里至少有一个键在表中已存在,则不会插入任何对象数据。

代码:

1> ets:new(test,[set,named_table]).
test
2> ets:insert(test,{a,1}).
true
3> ets:insert_new(test,{a,1}).
false
4> ets:insert_new(test,{b,1}).
true
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值