You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单核心环境下Erlang两种gen_tcp监听模型的性能差异探究:gen_tcp accept vs OS线程accept

Erlang监听套接字两种实现的单核心性能深度分析

让我们先拆解这两种Erlang监听套接字与Acceptor的实现,再聚焦单核心场景下的性能差异问题:

第一种:单进程循环Accept模型

这是最直观的实现方式,代码逻辑非常简洁:

-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) -> 
    % 套接字数据读取、业务逻辑处理等代码
    ....

逻辑解释:主进程创建监听套接字后,就进入一个死循环——每接收到一个客户端连接,就fork一个独立进程去处理这个连接的后续逻辑,自己立刻回到等待状态,准备接受下一个连接。

第二种:基于Supervisor的预启动Acceptor模型

这种实现依赖Erlang的监管树机制,提前启动多个Acceptor进程:

-module(listener).
% 省略初始化、配置等辅助代码
start() -> 
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).  % 启动监管进程

init([]) -> 
    {ok, Listen} = gen_tcp:listen(....),  % 创建监听套接字
    spawn(fun() -> free_acceptors(5) end), % 启动临时进程,用于后续创建Acceptor子进程
    % 配置simple_one_for_one类型的监管策略
    {ok, {{simple_one_for_one, 5, 1},
          [{child, {?MODULE, accept, [Listen]},
            % 子进程启动选项(如临时进程、自动重启等)
            temporary, brutal_kill, worker, [?MODULE]}]}}.

free_acceptors(N) -> 
    % 批量启动N个Acceptor子进程
    [supervisor:start_child(?MODULE, []) || _ <- lists:seq(1, N)], 
    ok.

accept(Listen) -> 
    {ok, Socket} = gen_tcp:accept(Listen),  % 阻塞等待新连接
    handle(Socket).                         % 处理连接(处理完后会再次调用accept等待)

handle(Socket) -> 
    % 套接字数据读取、业务逻辑处理等代码
    ....

逻辑解释:主进程启动一个simple_one_for_one类型的Supervisor,在监管进程的init/1方法中创建监听套接字,然后通过一个临时进程启动5个Acceptor子进程(因为Supervisor在初始化阶段无法直接启动子进程,必须等自身初始化完成后再操作)。这5个Acceptor进程会各自调用gen_tcp:accept/1,同时等待新连接到来。


单核心场景下的性能疑问与本质分析

我一开始也想当然认为第二种模型在单核心机器上处理5个并发连接会更快——毕竟5个Acceptor同时在等,看起来能并行接受连接,而第一种得一个接一个处理,第5个客户端要等前4个都被Accept完才能轮到。但深入研究ERTS(Erlang运行时系统)的工作原理后,这个想法就站不住脚了:

  • 单核心下的Erlang进程是并发而非并行:单核心机器上,ERTS默认只会启用一个OS线程作为调度器,所有Erlang进程都是通过调度器的快速切换来实现并发执行,看起来像并行,但本质上同一时间只有一个进程在执行。
  • OS层面的Accept操作是串行的:套接字是OS层面的资源,gen_tcp:accept/1最终会调用OS的accept系统调用。而OS对同一个监听套接字的accept调用有个硬性限制:同一时间只能有一个accept调用在处理连接请求。哪怕你启动了5个Erlang进程去调用accept,这些调用最终都会在OS层面排队,由单个OS线程依次处理。

结论:单核心下两种实现的性能差异微乎其微

回到问题场景:当5个客户端同时连接两种服务器时,两种模型的处理本质是一致的——所有连接请求都会被OS依次Accept,Erlang进程的调度切换不会改变OS层面的处理顺序。

两种实现的性能差异几乎可以忽略:

  • 第一种模型的主进程在Accept后Spawn处理进程,然后立刻回到Accept循环,调度开销极小。
  • 第二种模型的5个Acceptor进程在等待时会被调度器挂起,当有连接进来时,OS唤醒其中一个Acceptor完成Accept,处理完后该Acceptor会再次调用Accept等待下一个,这里的调度切换开销和第一种模型相比没有显著差别。

最后补充个小细节:业界知名的Erlang项目中,Ejabberd采用第一种单循环模型,而Cowboy采用第二种预启动Acceptor模型。这更多是架构设计的选择而非单核心性能的考虑——Cowboy的模型在多核机器上可以充分利用多个调度器(ERTS会为每个核心启动一个OS线程),让不同的Acceptor进程在不同核心上并行处理连接;而Ejabberd的单循环模型更简洁,在稳定场景下足够可靠。

内容的提问来源于stack exchange,提问作者Michael

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 21:12:37