Erlang gen_tcp 接受与 OS-Thread 接受

Erlang gen_tcp accept vs OS-Thread accept

我在 Erlang 中有两种监听套接字和接受器的模型:

------------第一个------------

-module(listeners).
....

start() ->
{ok, Listen}=gen_tcp:listen(....),
accept(Listen).

%%%%%%%%%%%%%%%%%%%%%

accept(Listen) ->
{ok, Socket}=gen_tcp:accept(Listen),
spawn(fun() ->handle(Socket) end),
accept(Listen).

%%%%%%%%%%%%%%%%%%%%%

handle(Socket) ->
.... 

------------第二个----------

-module(listener).
....

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

%%%%%%%%%%%%%

init([]) ->
{ok, Listen}=gen_tcp:listen(....),
spawn(fun() ->free_acceptors(5) end), 
{ok, {{simple_one_for_one, 5,1},[{child,{?MODULE,accept,[Listen]},....}]}.

%%%%%%%%%%%%%

free_acceptors(N) ->
[supervisor:start_child(?MODULE, []) || _ <-lists:seq(1,N)],
ok.

%%%%%%%%%%%%%

accept(Listen) ->
{ok, Socket}=gen_tcp:accept(Listen). 
handle(Socket). 

%%%%%%%%%%%%%%

handle(Socket) ->
.... 

第一个代码很简单,主进程创建一个侦听套接字并侦听接受新连接,当连接到来时,它接受连接产生一个新进程来处理它,returns接受其他新连接。

第二个代码也很简单,主进程创建一个监督树,监督者创建一个监听套接字并启动5个子进程(产生一个新进程到运行 free_acceptors/1因为这个函数调用主管进程和主管在它的初始化函数中,它不能在它自己启动之前启动子进程,所以新进程将等待主管直到它完成启动)并将监听套接字作为它的孩子的参数,并且五个 children 开始收听以同时接受新的连接。

所以我们 运行 这两个代码各自在一台单独的机器上,它们有一个 CPU 单核和 5 个客户端尝试同时连接到第一个服务器和其他 5 个到第二个服务器:从第一眼看,我认为第二个服务器更快,因为所有连接都将被并行接受,同时在第一个代码中,第 5 个客户端将等待服务器接受先例,第 4 个接受它等等。 但是深入 ERTS,我们每个核心有一个 OS-Thread 来处理 erlang 进程,并且由于 Socket 是一个 OS 结构,那么 gen_tcp:listen 将调用 OS-Thread:listen(这是只是需要理解的伪代码)创建一个 OS 套接字,然后 gen_tcp:accept 调用 OS-Thread:accept 接受新连接,之后一次只能接受一个连接,第五个客户端仍在等待服务器接受第四个先例,那么这两个代码之间有区别吗?我希望你能理解我。

即使代码不包含套接字,Erlang 进程也将始终是并发的而不是并行的,因为只有一个内核,但调度器将非常快速地管理进程之间的任务并接近并行 运行 ,所以问题在于使用跨单个 OS-Thread.

调用 OS 的套接字

注意:Ejabberd 使用第一个实现,Cowboy 使用第二个。

在 OS 级别,侦听套接字关联了一个等待接受连接的 OS 线程队列,无论该队列是否有任何 OS 线程阻塞在其上or 为空,因为它将以不同的方式处理(忙等待非阻塞接受,select,epoll...)。

BEAM 没有单个 OS 线程,即使您 运行 它在具有单个 CPU 的系统上,它也有 different types of OS-threads

关于你的问题,我怀疑如果有的话,让多个接受者 erlang 线程连续阻塞在 gen_tcp:accept 调用上会更好,因为这样 ERTS 就知道愿意接受的 erlang 代码更多连接(第二个示例中的 handle(Socket) 应该产生一个工作人员或将接受的套接字发送给工作人员并返回接受连接),而对于单个接受产生循环,此知识是隐藏的。

我对 code 不够熟悉,无法了解其中的细微差别,但代码似乎可以很好地处理多个接受,在内部对它们进行排队,因此拥有多个接受者可能稍微好一些。
IE。在单个请求的第一个示例中,有一段时间没有人 accepting 连接,而在第二个示例中您需要更多的同时请求才能发生这种情况。

我通过大量搜索找到了答案,所以我想我找到了答案,但如果我有任何错误,我只想纠正我 José 先生:

1-当我们 运行 gen_tcp:listen ERTS 打开一个 ERLANG 端口(一个监听套接字)与 linked-in C 驱动程序通信时,这个驱动程序 运行s 在 MAIN OS 下 - 线程打开一个 REAL SOCKET。

2-当我们 运行 gen_tcp:accept ERTS 使用此端口调用驱动程序时,使用指定的宏作为函数 erlang:port_control 的参数,驱动程序 MAIN OS-Thread 将产生一个 OS-Thread,它将 运行 在打开的套接字处接受一个真正的接受(阻塞接受)但这只是我的看法我也是我不熟悉 C Accept函数,反正这是爱立信团队的工作。

3-当客户端发送请求连接到此套接字时,OS-线程接受连接并创建一个与此客户端通信的新套接字,Erlang 进程创建一个新端口和link 它到此 OS-线程作为与该客户端的此指定通信的驱动程序。

4-当 Erlang 进程通过这个新端口发送数据时,新驱动程序通过新套接字发送数据,接收数据也是如此。

5-MAIN OS-Thread 驱动程序不会在每个 Erlang Accept 产生一个新的 OS-Thread 并且会在 OS-Threads 之间进行平衡和连接(这又是爱立信设计),这些线程将管理与已知功能之一的连接(select、轮询、epoll、......),通常它是 epoll 用于 Linux 和 Kqueue 对于 Bsd 系统和每个 OS-Thread 将 运行 此函数在两侧:一侧与客户端套接字交互,一侧与 Erlang 端口交互。

这是任何驱动程序的确切工作,它隐藏了一些东西,让模拟器的行为就像它直接完成工作一样。

第一个问题的答案是第二个代码更有效,就像你告诉我的那样,当有很多 Erlang-Acceptors 时驱动程序知道这一点并产生许多 OS-Acceptors,在这里还有另一个问题:我可以为一个套接字生成多少个接受器?

自由接受器设计用于接受并行连接,很明显,一个 OS-线程不能同时接受两个连接,因此如果接受器数量大于核心数量例如,如果我们有 8 个核心和 10 个接受器,同时有 20 个客户端,我们有 8 个并行接受的连接,接下来是另外 8 个并行连接,接下来是 4 个,所以这与我们创建 8 个免费接受器的效率相同。(我当我们总是有 8 个空闲接受器并且当一个接受器接受一个连接时,它会产生一个进程来处理这个连接并 return 接受其他连接)

讨论正确的代码版本

网络是在 Erlang/OTP 中设计容错和可扩展服务器的最重要部分,我想在做任何事情之前很好地理解它,所以请 José 先生如果我有什么不对的地方请告诉我谢谢。