2

当许多 Erlang 进程同时调用 C NIF 时,我观察到它们的阻塞行为。可以做到无阻塞吗?这里有mutex我无法理解的工作吗?

PS 一个基本的“Hello world”NIF 可以通过将其设为sleep100来进行测试microseconds,以防特定PID调用它。可以观察到,调用 NIF 的其他 PID 在执行之前等待该睡眠执行。

在并发可能不会造成问题的情况下(例如数组推送、计数器增量),非阻塞行为将是有益的。

我正在分享 4 个要点的链接,这些要点分别由 a和spawnermodule组成。我试图修改 的值,并且确实观察到了非阻塞行为。这可以通过为函数分配一个大整数参数来确认。conc_nif_callerniftestValspawn_multiple_nif_callers

链接 spawner.erlconc_nif_caller.erlniftest.erl和最后的 niftest.c

下面的行是由我的 Mac 上的 Erlang REPL 打印的。

Erlang/OTP 17 [erts-6.0] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
4

3 回答 3

5

NIF 本身没有任何互斥锁。你可以在 C 中实现一个,当你加载 NIF 的对象时也有一个,但这应该只在加载模块时完成一次。

可能正在发生的一件事(我敢打赌这就是正在发生的事情),是你的 C 代码弄乱了 Erlang 调度程序。

在返回之前执行冗长工作的本机函数会降低 VM 的响应能力,并可能导致各种奇怪的行为。这种奇怪的行为包括但不限于极端的内存使用和调度程序之间的不良负载平衡。由于冗长的工作可能发生的奇怪行为也可能因 OTP 版本而异。

描述什么lengty work意思以及如何解决它。

用很少的话(很少简化):

对于核心,创建了一个调度程序。每个人都有一个他可以运行的进程列表。如果一个调度器列表为空,他将尝试从另一个调度器继续工作。如果没有什么(或不够)可以静止,这可能会失败。

Erlang 调度程序在一个进程中花费一些工作,然后转移到另一个,在那里花费一些工作,然后转移到另一个。等等,等等。这与系统进程中的调度非常相似。

这里非常重要的一件事是计算工作量。默认情况下,每个函数调用都分配了一些减少量。加法可能有两个,模块中的调用函数将有一个,发送消息也是一个,某些内置可能有更多(如list_to_binary)。如果我们收集到 2 000 个减少量,我们将转移到另一个过程。

那么你的 C 函数的成本是多少?这只是一个减少。

像这样的代码

loop() ->
   call_nif_function(),
   loop().

可能会占用一整个小时,但调度程序将被困在这一进程中,因为他还没有计算到 2 000 次减少。或者换句话说,他可能会被困在 NIF 内而无法继续前进(至少在短期内)。

解决这个问题的方法很少,但一般规则是统计 NIF 不应该花费很长时间。因此,如果您有长时间运行的 C 代码,也许您应该使用驱动程序。它们应该更容易实施和管理,即修补 NIF。

于 2014-10-23T18:09:35.300 回答
4

NIF 调用会阻塞调用它们的进程所绑定的调度程序。因此,对于您的示例,如果那些其他进程在同一个调度程序上,则在第一个进程完成之前,它们无法调用 NIF。

在这方面,您不能进行非阻塞的 NIF 调用。但是,您可以生成自己的线程并将您的工作首当其冲卸载给它们。

这样的线程可以向本地Erlang 进程(同一台机器上的进程)发送消息,因此您仍然可以通过等待生成的线程发回消息来获得所需的响应。

一个不好的例子:

static ERL_NIF_TERM my_function(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    MyStruct* args = new MyStruct(); // I like C++; so sue me
    args->caller = enif_self();
    ErlNifTid thread_id;
    // Please remember, you must at some point rejoin the thread, 
    // so keep track of the thread_id
    enif_thread_create("my_function_thread", &thread_id, my_worker_function, (void*)args, NULL);
    return enif_make_atom(env, "ok");
}
void* my_worker_function(void* args) {
    sleep(100);
    ErlNifEnv* msg_env = enif_alloc_env();
    ERL_NIF_TERM msg = enif_make_atom(msg_env, "ok");
    enif_send(NULL, args->caller, msg_env, msg);
    delete args;
    return NULL;
}

在您的 erlang 源代码中:

test_nif() -> 
    my_nif:my_function(),
    receive
        ok -> ok
    end.

无论如何,有这样的效果。

于 2014-10-23T18:03:20.637 回答
4

我认为关于长期运行的 NIF 的回答是不合时宜的,因为您的问题说您正在运行一些简单的“hello world”代码并且只睡了 100 我们。确实,理想情况下,NIF 调用不应超过一毫秒,但您的 NIF 可能不会导致调度程序问题,除非它们一次持续运行数十毫秒或更长时间。

我有一个简单的 NIF 调用rev/1,它接受一个字符串参数,反转它,并返回反转的字符串。我usleep在它中间插入了一个调用,然后生成了 100 个并发 Erlang 进程来调用它。下面显示的两个线程堆栈跟踪基于 Erlang/OTP 17.3.2,rev/1同时显示了 NIF 内部的两个 Erlang 调度程序线程,一个在我在 NIF C 函数本身设置的断点处,另一个在 NIFusleep内部阻塞:

Thread 18 (process 26016):
#0  rev (env=0x1050d0a50, argc=1, argv=0x102ecc340) at nt2.c:9
#1  0x000000010020f13d in process_main () at beam/beam_emu.c:3525
#2  0x00000001000d5b2f in sched_thread_func (vesdp=0x102829040) at beam/erl_process.c:7719
#3  a0x0000000100301e94 in thr_wrapper (vtwd=0x7fff5fbff068) at pthread/ethread.c:106
#4  0x00007fff8a106899 in _pthread_body ()
#5  0x00007fff8a10672a in _pthread_start ()
#6  0x00007fff8a10afc9 in thread_start ()

Thread 17 (process 26016):
#0  0x00007fff8a0fda3a in __semwait_signal ()
#1  0x00007fff8d205dc0 in nanosleep ()
#2  0x00007fff8d205cb2 in usleep ()
#3  0x000000010062ee65 in rev (env=0x104fcba50, argc=1, argv=0x102ec8280) at nt2.c:21
#4  0x000000010020f13d in process_main () at beam/beam_emu.c:3525
#5  0x00000001000d5b2f in sched_thread_func (vesdp=0x10281ed80) at beam/erl_process.c:7719
#6  0x0000000100301e94 in thr_wrapper (vtwd=0x7fff5fbff068) at pthread/ethread.c:106
#7  0x00007fff8a106899 in _pthread_body ()
#8  0x00007fff8a10672a in _pthread_start ()
#9  0x00007fff8a10afc9 in thread_start ()

如果 Erlang 模拟器中有任何互斥锁阻止并发 NIF 访问,则堆栈跟踪不会显示 C NIF 中的两个线程。

如果您要发布您的代码,那么那些愿意帮助解决此问题的人可以看到您在做什么,也许可以帮助您找到任何瓶颈。如果您告诉我们您使用的是什么版本的 Erlang/OTP,也会很有帮助。

于 2014-10-23T19:09:53.520 回答