【问题标题】:Erlang: blocking C NIF call behaviorErlang:阻止 C NIF 调用行为
【发布时间】:2014-12-19 07:47:42
【问题描述】:

我观察到 C NIF 在被许多 Erlang 进程同时调用时会出现阻塞行为。可以做到无阻塞吗?这里有没有我无法理解的mutex

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

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

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

链接 spawner.erl,conc_nif_caller.erl,niftest.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]

【问题讨论】:

    标签: c concurrency erlang erlang-nif


    【解决方案1】:

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

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

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

    description lengty work 的含义以及如何解决它。

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

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

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

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

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

    类似代码

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

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

    around this 有几种方法,但一般规则是统计 NIF 不应该花费很长时间。所以如果你有长时间运行的 C 代码,也许你应该改用drivers。它们应该更容易实施和管理,即修补 NIF。

    【讨论】:

    • 就像一个注释一样,有人谈论用 NIF 完全替换 C 端口。这还有一段路要走(如果它甚至会发生的话),但社区内的普遍共识是,NIF 现在或至少正在迅速成为首选的本地执行方法。
    • 我不同意。首先,NIF 对某些事情很好,但驱动程序仍然非常重要,特别是对于网络子系统和其他可以利用 Erlang 模拟器的文件描述符轮询功能的领域(当然,你可以在 NIF 中自己实现它,但是为什么要复制什么?已经为您提供了便携式服务?)。其次,如果有任何东西取代了驱动程序,它将是本机进程,而不是 NIF,但本机进程并非微不足道,因此仍然是未来的工作项目。
    • 本地进程是我在这里提到 NIF 时所得到的(例如,我在另一个评论线程中提到的那个提议,因此我建议它是“一个方法”)。对于我的术语混合造成的任何混乱,我深表歉意。最终,我确实看到驱动程序被逐步淘汰,取而代之的是原生进程和扩展 NIF(NIF API 更容易使用,IMO)。当然,在 VM 内部方面,您似乎比我更精通,所以我会在这里听从您的意见。
    • @Soupd'Campbells 是也不是。简单的 NIF 很简单。长时间运行的不是那么多。您可以看到 NIF 方面做了很多工作,但有些功能是实验性的,您必须引入额外的复杂性。甚至更多的是“不同类型”的复杂性,必须以非标准的 Erlang 方式处理(理解、管理和调试)。因此,虽然一些新功能真的很酷,但我会将它们视为最后的资源。驱动程序是并将成为标准库的一部分。他们工作,而且他们工作得很好。如果没有新功能,只是因为没有必要。恕我直言。
    【解决方案2】:

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

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

    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,也会很有帮助。

    【讨论】:

    • 我的回答不是针对长时间运行的 NIF 调用,而是关于他正在运行的模拟器是否启用了 SMP,如果是,它是否有多个调度程序。在单调度程序场景中,调用 sleep 的 NIF 在技术上会阻塞其他进程,但实际上只是阻止调度程序访问它们。因此,用户线程或脏调度程序(如您所建议的)将允许(干净的)调度程序恢复运行其他进程。
    • 是的,你是对的。它确实是非阻塞的。我已经分享了代码的链接。我在玩较小的值,导致我之前的错误结论。
    • @abips - Erlang 对 mpm 在他的回答中描述的高级进程有一个特殊的负载平衡机制。具体来说,Erlang 尝试将尽可能多的进程绑定到单个调度程序,目标是用可运行的进程使该调度程序的 CPU 时间饱和(并迁移多余的进程)。这会影响 spawn,因为新进程通常会在进行 spawn 调用的调度程序上开始。因此,在工作人员数量较少的情况下,您更有可能观察到似乎阻碍睡眠的行为,但这只是所有进程都在一个(睡眠)调度程序上。
    • @Soupd'Campbells 感谢您的解释。绝对有道理。
    【解决方案3】:

    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.
    

    总之就是这样。

    【讨论】:

    • 作为一个注释,我认为创建一个线程来处理每个进来的请求通常不是一个好主意。你最好创建一些“工作线程”和通过一些受锁保护的资源(或一些漂亮的无锁数据结构)与他们通信。
    • 使用 Erlang 17,如果您有长时间运行的 NIF 任务,您应该将它们卸载到 dirty schedulers,而不是编写自己的线程池。
    • 是和不是。事情可能已经发生了变化,但最后我检查了脏调度程序仍然是“实验性的”,并且实现可能会发生变化。在它们成为最终功能之前,最好不要将它们用于生产环境中的任何东西。
    • 我写的。在这一点上,它们不太可能发生变化。
    • 很高兴知道。我听说过一些关于让脏调度程序运行我在初始规范中没有看到的有效完整的 C 进程的有趣传闻(这些进程将是面向回调的,并且具有调用 Erlang 函数或转换为纯 Erlang 进程的接口彻底)。抱有希望,我们可能会在未来看到其中的一些。
    猜你喜欢
    • 2016-03-30
    • 2020-05-17
    • 2013-01-03
    • 2013-01-17
    • 2013-01-03
    • 2016-10-06
    • 2014-12-17
    • 2015-01-02
    • 2012-01-07
    相关资源
    最近更新 更多