Nif concurrency strategies
View SourceWhen execution flow enters a Nif, control is fully relinquished from the managed environment of the BEAM VM to a context where the BEAM is more or less unaware of what is going on.
In general the VM cannot tolerate native code running for longer than approximately one millisecond.
There are several tools that the BEAM nif system provides for you to
Synchronous
The default mode for Nifs to run is synchronous. Only use this mode if you are confident that your code can run in under 1ms.
Dirty CPU
dirty_cpu mode is usable when your VM has created Dirty CPU schedulers. By default, the VM
creates one dirty CPU scheduler per CPU core available to it. Nifs tagged as dirty_cpu are allowed
to run longer than 1 millisecond.
In order to tag a function as dirty_cpu, use the :dirty_cpu flag in the options list for the
function in the :nif call.
beam.yield in Dirty CPU
The beam.yield function in dirty CPU mode will detect if the parent process has
died and will return error.processterminated.
defmodule DirtyCpu do
use ExUnit.Case, async: true
use Zig,
otp_app: :zigler,
nifs: [long_running: [:dirty_cpu]]
~Z"""
const beam = @import("beam");
// this is a dirty_cpu nif.
pub fn long_running(pid: beam.pid) !void {
defer {
// code in the defer block is triggered when process is killed.
// we need to create a new thread-independent context because
// the context from the running process is now invalid.
const env = beam.alloc_env();
beam.send(pid, .killed, .{.env = env}) catch unreachable;
beam.free_env(env);
}
try beam.send(pid, .unblock, .{});
while(true) {
try beam.yield();
}
}
"""
test "dirty cpu can be cancelled" do
this = self()
dirty_cpu = spawn(fn -> long_running(this) end)
assert_receive :unblock
Process.exit(dirty_cpu, :kill)
assert_receive :killed
end
endqueue limitations
if you consume all of your dirty cpu schedulers with nif calls, the next dirty_cpu call will block
until a scheduler frees up; this could cause undesired latency characteristics.
Dirty IO
It's not recommended to use dirty_io unless you're performing IO operations and blocking using nif
events and blocking operations.
In order to tag a function as dirty_io, use the :dirty_io flag in the options list for the
function in the :nif call.
Threaded
threaded mode is usable when your OS supports spawning threads. This is effectively all current
platforms supporting the BEAM VM today. Zigler will wrap your function code
In order to tag a function as threaded, use the :threaded flag in the options list for the
function in the :nif call. Generally, no other changes must be made to execute a function in
threaded mode.
env in Threaded mode
The env variable when you run in threaded mode is not a process-bound environment.
beam.yield in Threaded mode
The beam.yield function in dirty CPU mode will detect if the parent process has
died and will return error.processterminated.
return from yield quickly!
You must return from the yield quickly (within 750us). If you are unable to return quickly, then zigler run will cause the thread metadata to leak. This will be fixed in zigler 0.11.
defmodule Threaded do
use ExUnit.Case, async: true
use Zig,
otp_app: :zigler,
nifs: [long_running: [:threaded]]
~Z"""
const beam = @import("beam");
const std = @import("std");
pub fn long_running(pid: beam.pid) !void {
// following code triggered when process is killed.
defer {
beam.send(pid, .killed, .{}) catch {};
}
while(true) {
_ = try beam.send(pid, .unblock, .{});
try beam.yield();
}
}
"""
@tag :threaded
test "threaded can be cancelled" do
this = self()
threaded = spawn(fn -> long_running(this) end)
#assert_receive :unblock
Process.sleep(100)
Process.exit(threaded, :kill)
assert_receive :killed
Process.sleep(1000)
end
endYielding
yielding nifs
Yielding nifs are not available in this release of Zigler
# module