Erlang: блокировка поведения вызова C NIF

Я наблюдал блокирующее поведение C NIF, когда они вызывались одновременно многими процессами Erlang. Можно ли сделать его неблокирующим? Здесь работает mutex, которого я не могу понять?

P.S. Базовый NIF "Hello world" можно протестировать, сделав его sleep на сто microseconds в случае, если его вызывает конкретный PID. Можно заметить, что другие PID, вызывающие NIF, ждут выполнения этого сна перед их выполнением.

Неблокирующее поведение было бы полезно в тех случаях, когда параллелизм может не представлять проблемы (например, отправка массива, увеличение счетчика).

Я делюсь ссылками на 4 списка, которые состоят из модулей spawner, conc_nif_caller и niftest соответственно. Я пытался возиться со значением Val и действительно наблюдал неблокирующее поведение. Это подтверждается назначением большого целочисленного параметра функции spawn_multiple_nif_callers.

Ссылки spawner.erl, conc_nif_caller.erl и наконец, niftest.c.

Строка ниже напечатана Erlang REPL на моем Mac.

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

person abips    schedule 23.10.2014    source источник


Ответы (3)


Я думаю, что ответы о долго работающих NIF не соответствуют действительности, поскольку в вашем вопросе говорится, что вы используете какой-то простой код «hello world» и спите всего 100 человек. Это правда, что в идеале вызов NIF не должен занимать более миллисекунды, но ваши NIF, скорее всего, не вызовут проблем с планировщиком, если только они не выполняются последовательно в течение десятков миллисекунд за раз или более.

У меня есть простой NIF с именем rev/1, который принимает строковый аргумент, переворачивает его и возвращает перевернутую строку. Я вставил вызов usleep в его середину, а затем создал 100 одновременных процессов Erlang для его вызова. Две трассировки стека потоков, показанные ниже, основанные на Erlang/OTP 17.3.2, показывают два потока планировщика Erlang внутри rev/1 NIF одновременно, один в точке останова, которую я установил в самой функции NIF C, другой заблокирован в usleep внутри 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 вы используете.

person Steve Vinoski    schedule 23.10.2014
comment
Мой ответ был не столько ориентирован на длительные вызовы NIF, сколько на то, поддерживает ли эмулятор, на котором он работает, SMP, и если да, то имеет ли он несколько планировщиков. В сценарии с одним планировщиком NIF, вызывающий спящий режим, технически блокирует другие процессы, но на самом деле просто блокирует доступ планировщика к ним. Таким образом, пользовательский поток или грязный планировщик (как вы предложили) позволит (чистому) планировщику возобновить выполнение других процессов. - person Soup d'Campbells; 23.10.2014
comment
Да, ты прав. Он действительно не блокирует. Я поделился ссылками на код. Я играл с меньшими значениями, что привело к ошибочному заключению моего ранее. - person abips; 24.10.2014
comment
@abips - Erlang имеет особый механизм балансировки нагрузки для процессов, описанных на высоком уровне mpm в его ответе. В частности, Erlang пытается связать как можно больше процессов с одним планировщиком с целью насыщения процессорного времени этого планировщика исполняемыми процессами (и миграции излишков). Это влияет на порождение, поскольку новые процессы часто начинаются в планировщике, где выполняется вызов порождения. Следовательно, с небольшим количеством рабочих процессов вы, скорее всего, заметите то, что блокирует поведение от сна, но просто все процессы находятся в одном (спящем) планировщике. - person Soup d'Campbells; 24.10.2014
comment
@Soupd'Campbells Спасибо за объяснение. Определенно имеет смысл. - person abips; 24.10.2014

Сами NIF не имеют мьютексов. Вы можете реализовать один на C, и один при загрузке объекта NIF, но это нужно сделать только один раз с загрузочным модулем.

Одна вещь, которая может произойти (и я держу пари, что это то, что происходит), это то, что ваш код C портит планировщик(и) Erlang.

Собственная функция, которая выполняет длительную работу перед возвратом, ухудшит реакцию виртуальной машины и может вызвать странное поведение. Такое странное поведение включает, помимо прочего, чрезмерное использование памяти и неправильную балансировку нагрузки между планировщиками. Странное поведение, которое может возникнуть из-за длительной работы, также может варьироваться в зависимости от выпуска OTP.

и описание того, что означает lengty work и как вы можете решить эту проблему.

В двух словах (с небольшими упрощениями):

Для ядра создан один планировщик. У каждого есть список процессов, которые он может запускать. Если его список планировщика пуст, он попытается продолжить работу с другого. Это может потерпеть неудачу, если нечего (или недостаточно) еще.

Планировщики Erlang тратят какое-то количество работы в одном процессе, затем перемещаются в другой, тратят там какое-то количество работы и переходят в другое. И так далее, и так далее. Это очень похоже на планирование в системных процессах.

Здесь очень важно рассчитать объем работы. По умолчанию каждому вызову функции назначено некоторое количество сокращений. У дополнения может быть два, у вызывающей функции в вашем модуле будет один, у отправки сообщения также один, у некоторых встроенных может быть больше (например, list_to_binary). Если мы набираем 2 000 сокращений, мы переходим к другому процессу.

Итак, какова стоимость вашей C-функции? Это только одно сокращение.

Код как

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

может занять целый час, но планировщик застрянет на этом одном процессе, потому что он еще не досчитал до 2 000 сокращений. Или, другими словами, он может застрять внутри НИФ без возможности двигаться вперед (по крайней мере, в ближайшее время).

Есть несколько способов обойти это, но общее правило таково: много времени. Поэтому, если у вас есть длинный код C, возможно, вам следует использовать драйверы. Их должно быть намного проще реализовать и управлять ими, чем возиться с NIF.

person mpm    schedule 23.10.2014
comment
Как примечание, ходили разговоры о полной замене портов C на NIF. До этого еще далеко (если это вообще произойдет), но общий консенсус в сообществе заключается в том, что NIF сейчас или, по крайней мере, быстро становятся предпочтительным методом собственного исполнения. - person Soup d'Campbells; 23.10.2014
comment
Я не согласен. Во-первых, NIF удобен для некоторых вещей, но драйверы по-прежнему очень важны, особенно для сетевых подсистем и других областей, которые могут воспользоваться возможностями опроса файловых дескрипторов эмулятора Erlang (конечно, вы можете реализовать это самостоятельно в NIF, но зачем дублировать то, что есть). уже переносимо для вас?). Во-вторых, если что-то и заменит драйверы, то это будут нативные процессы, а не NIF, но нативные процессы не являются тривиальными и поэтому остаются предметом будущей работы. - person Steve Vinoski; 23.10.2014
comment
Собственные процессы — это то, что я имел в виду, когда говорил здесь о NIF (например, то предложение, о котором я упоминал в другой ветке комментариев, и поэтому я предполагаю, что это далеко). Прошу прощения за путаницу, вызванную моим смешиванием терминов. В конечном итоге я вижу, что драйверы постепенно отказываются от использования собственных процессов и расширенных NIF (NIF API намного проще в использовании, IMO). Конечно, вы, кажется, лучше меня разбираетесь во внутреннем устройстве виртуальной машины, поэтому я уступлю вам здесь. - person Soup d'Campbells; 24.10.2014
comment
@Soupd'Campbells да и нет. Простые NIF просты. Долгоиграющих не так много. Вы можете видеть, что многое делается в рамках NIF, но некоторые функции являются экспериментальными, и вам нужно внести дополнительную сложность. Более того, это разные виды сложности, с которыми нужно обращаться (понимать, управлять и отлаживать) нестандартным способом Erlang. Так что, хотя некоторые новые функции действительно крутые, я бы рассматривал их как последний ресурс. Драйверы есть и будут частью стандартной библиотеки. Работают, и работают хорошо. Если нет новых функций, то только потому, что в этом нет необходимости. ИМХО. - person mpm; 24.10.2014

Вызовы 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.

Во всяком случае, что-то в этом роде.

person Soup d'Campbells    schedule 23.10.2014
comment
Просто в качестве примечания: я не думаю, что в целом хорошей идеей является создание потока для обработки каждого входящего запроса. Вам лучше создать несколько рабочих потоков и общаться с ними через какой-либо защищенный блокировкой ресурс ( или какую-нибудь изящную структуру данных без блокировки). - person Soup d'Campbells; 23.10.2014
comment
В Erlang 17, если у вас есть длительные задачи NIF, вы должны перенести их на грязные планировщики вместо написания собственных пулов потоков. - person Steve Vinoski; 23.10.2014
comment
Да и нет. Возможно, что-то изменилось, но последний раз, когда я проверял, грязные планировщики все еще были экспериментальными, и реализация, вероятно, изменится. Лучше не использовать их ни для чего в производственной среде, пока они не станут окончательной функцией. - person Soup d'Campbells; 23.10.2014
comment
Я написал их. На данный момент они вряд ли изменятся. - person Steve Vinoski; 23.10.2014
comment
Хорошо знать. Я слышал некоторые интересные слухи о том, что грязные планировщики запускают то, что фактически является завершенными процессами C, которых я не видел в исходной спецификации (такие процессы будут ориентированы на обратный вызов и будут иметь интерфейсы для вызова функций Erlang или преобразования в процесс на чистом Erlang). сразу). Надеясь, что мы можем увидеть некоторые из них в будущем. - person Soup d'Campbells; 23.10.2014
comment
Если вам интересно, те слухи, которые я услышал, исходили из этой презентации: erlang-factory.com/upload/presentations/377/ Похоже, это не EEP... пока. - person Soup d'Campbells; 23.10.2014