一、Erlang调度器是什么?

想象你开了一家快递公司,有10个快递员(CPU核心)和100个包裹(任务)。Erlang调度器就是那个聪明的调度员,它决定哪个快递员在什么时候送哪个包裹,而且还能在快递员累的时候自动调整工作量。

Erlang的调度器专门为并发设计,它不像传统系统那样让一个任务霸占CPU不放,而是把任务切成小片段轮流执行。比如你有一个计算圆周率的超级耗时任务,调度器会把它拆成许多小块,和其他小任务(比如处理用户请求)穿插着执行,这样系统就不会"卡死"。

二、调度器如何优化CPU密集型任务

1. 时间片轮转:公平分蛋糕

每个Erlang进程默认只有1-2毫秒的执行时间(称为reduction),时间一到就被暂停换下一个。比如:

%% 技术栈:Erlang/OTP 25+
%% 模拟一个耗时计算
calculate_pi(0) -> 3.14159;
calculate_pi(N) -> 
    do_heavy_math(),  % 假装这里有很多数学运算
    calculate_pi(N-1).

调度器会让这段代码执行一小会儿,然后切换到其他进程,比如处理HTTP请求的进程。这样即使计算圆周率要10秒,网页服务也不会完全没响应。

2. 多级队列:VIP通道机制

Erlang维护不同优先级的任务队列。比如系统级任务(如垃圾回收)可以插队,而普通任务则按先来后到。通过spawn_opt/4可以设置优先级:

%% 技术栈:Erlang/OTP 25+
{_, HighPriority} = spawn_opt(fun() -> 
    io:format("紧急日志处理~n") 
end, [priority, high]),  % 高优先级设置

3. 负载均衡:不累死任何一个核心

当你有8核CPU时,调度器会自动把400个进程均衡分配到所有核心。如果某个核心的任务队列太长,它会像借调员工一样把任务分给空闲核心。通过erlang:system_info(schedulers_online)可以看到正在工作的调度器数量。

三、实战:加速图像处理的例子

假设我们要用Erlang处理100张图片的缩略图生成:

%% 技术栈:Erlang/OTP 25+
process_images([]) -> done;
process_images([Img|Rest]) ->
    spawn(fun() -> 
        %% 模拟耗时操作
        timer:sleep(100),  
        io:format("处理 ~p~n", [Img])
    end),
    process_images(Rest).

%% 启动200个并行处理进程
start() -> 
    Images = ["img1.jpg", "img2.jpg"|...], % 假装有100个元素
    process_images(Images).

这里的关键是:

  1. 每个spawn创建轻量级进程(仅2-3KB内存)
  2. 调度器自动利用所有CPU核心
  3. 如果某个图片处理卡住,不会阻塞其他任务

四、什么时候该用这个方案?

适用场景:

  • 需要同时处理大量独立任务时(如批量数据处理)
  • 任务执行时间差异较大时(避免"短任务等待长任务")
  • 要求系统高可用(比如游戏服务器不能因为一个复杂计算导致整体卡顿)

需要注意:

  1. 不要过度并行:虽然Erlang进程很轻量,但创建10万个进程还是会增加调度开销
  2. IO密集型混合场景:如果任务大量依赖磁盘/网络,可能需要配合async
  3. 监控工具:用recon库查看调度器负载情况,避免某些核心长期过载

对比其他方案:

方案 优点 缺点
原生线程 无调度开销 容易死锁/内存泄漏
Go协程 更简单的语法 缺乏优先级控制
传统事件循环 适合IO密集型 一个卡住全盘皆输

五、总结

Erlang调度器就像个智能交通指挥系统,它通过:

  1. 时间分片 - 让每个任务都有机会执行
  2. 动态优先级 - 重要任务优先处理
  3. 自动负载均衡 - 榨干多核CPU的每一分性能

对于需要兼顾高并发和稳定性的CPU密集型任务,这套机制能在不增加复杂度的前提下,让你自然写出高效代码。下次当你面对需要大量计算的场景时,不妨试试Erlang这个"隐形加速器"。