近期对Erlang的NIF函数进行先期的学习和预研,在观看API文档时看到了NIF函数会抢占Erlang 虚拟机调度器线程的问题,导致其它Erlang进程无法正常使用调度器线程,由此阻塞系统及导致各种稀奇古怪的事情发生。
Erlang官方建议NIF函数返回速度相当快是非常重要的,通常一个行为良好的本地函数会在1毫秒内返回给调用者。如果一个NIF函数需要长时间运行,那么NIF的编写者需要对NIF函数进行切割,即分割成各个子NIF函数,这样能在执行完一个子NIF函数后,VM可以把调度器线程归还给线程池以供给其它进程使用。
怎么拆分NIF函数,有两个选项:
- 从Erlang级别进行一系列NIF调用。
- 调用一个NIF,该NIF首先执行一个工作块,然后调用enif_schedule_nif函数来调度另一个NIF调用以执行下一个块。然后,以这种方式调度的最后调用可以返回总体结果。
选项1Erlang级别调用子NIF比较容易理解,没有进行测试,这里讨论选项2的情况。为了进行测试,smp参数设置-smp +S 1
,验证是否能正常返回唯一的调度线程给其它进程使用。测试代码如下(每隔一秒打印一次“loop:n”):
static ERL_NIF_TERM loop_sleep(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]){
Sleep(1000);
int n = 0;
enif_get_int(env, argv[0], &n);
printf("loop:%d\n",n);
if(n > 0){
ERL_NIF_TERM new_argv[] = {enif_make_int(env, n-1)};
return enif_schedule_nif(env, "loop_sleep", 0, loop_sleep, 1, new_argv);
}else{
return atom_ok;
}
}
static ERL_NIF_TERM test(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]){
ERL_NIF_TERM new_argv[] = {enif_make_int(env, 10)};
return enif_schedule_nif(env, "loop_sleep", 0, loop_sleep, 1, new_argv);
}
Erlang进程代码如下(每隔一秒打印一次“heart:n~~”):
handle_info({heart,N}, State) ->
case N > 0 of
true ->
?INFO("heart:~w~~~~~n",[N]),
erlang:send_after(1000, self(), {heart,N-1});
_ ->
skip
end,
{noreply, State};
使用nif_server(跑erlang函数)和nif_server2(跑NIF函数)两个进程进行测试,不改变进程的优先级(默认normal),实测控制台打印如下:
点击查看结果
([email protected])12> nif_server:heart().
========= 2023-03-08 12:23:33 [info] nif_server#92 ========
heart:10~~
{heart,10}
([email protected])32>
========= 2023-03-08 12:23:34 [info] nif_server#92 ========
heart:9~~
([email protected])32>
========= 2023-03-08 12:23:35 [info] nif_server#92 ========
heart:8~~
([email protected])32> nif_server2:mfa(nif_random2,test,[{}]).
loop:10
loop:9
loop:8
loop:7
okloop:6
loop:5
loop:4
loop:3
loop:2
loop:1
loop:0
========= 2023-03-08 12:23:41 [info] nif_server#92 ========
heart:7~~
([email protected])33>
========= 2023-03-08 12:23:48 [info] nif_server#92 ========
heart:6~~
可以看起来貌似并不能正常将调度器线程归还(实际情况并不是),辛苦做的拆分NIF并没有什么用,尝试将
return enif_schedule_nif(env, "loop_sleep2", 0, loop_sleep, 1, new_argv);
改成return enif_schedule_nif(env, "loop_sleep", ERL_NIF_DIRTY_JOB_CPU_BOUND, loop_sleep, 1, new_argv)
:
点击查看结果
([email protected])12> nif_server:heart().
========= 2023-03-08 14:29:37 [info] nif_server#92 ========
heart:10~~
{heart,10}
([email protected])43>
========= 2023-03-08 14:29:38 [info] nif_server#92 ========
heart:9~~
([email protected])43>
========= 2023-03-08 14:29:39 [info] nif_server#92 ========
heart:8~~
([email protected])43> nif_server2:mfa(nif_random2,test,[{}]).
ok
([email protected])44>
========= 2023-03-08 14:29:40 [info] nif_server#92 ========
heart:7~~
([email protected])44> loop:10
========= 2023-03-08 14:29:41 [info] nif_server#92 ========
heart:6~~
([email protected])44> loop:9
========= 2023-03-08 14:29:42 [info] nif_server#92 ========
heart:5~~
([email protected])44> loop:8
这种情况倒是符合要求,只不过变成了脏调度函数,也就是两个调度器在虚拟机层面平行使用,自然不会互相影响,需要说明的是,虽然没有指定 +SDcpu,但是默认情况下脏调度线程数量同普通调度线程数量是一致的。
erl -name [email protected] -pa ../ebin -s game -smp +S 1
Eshell V9.3 (abort with ^G)
([email protected])1> erlang:system_info(dirty_cpu_schedulers_online).
1
那么上面的普通调度是怎么回事呢,其实问题是NIF函数所花费的时间片(reductions)是固定的,如果一次调度没有达到指定的reductions,而进程又还有任务可以继续处理,则调度器线程不会让给别的进程使用,这样做的目的是为了避免调度器在进程池里频繁切换进程带来多余的开销。那么解决办法就是,用enif_consume_timeslice重新帮助NIF函数重新估算reductions,道理就是告诉当前NIF函数该退出了,不要继续执行。这种写法想想明显不太适用普通业务型代码。。so,遵照官方的建议,一个行为良好的本地函数会在1毫秒内返回给调用者!!!更长的函数或者无法控制时间的第三方库,尝试使用os线程(也就是异步方式)或者脏调度吧。
标签:heart,127.0,0.1,NIF,nif,long,running,loop From: https://www.cnblogs.com/karl2013/p/17199362.html