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!
When the parent process dies or the thread resource is garbage collected, Zigler signals the thread
to terminate (causing beam.yield() to return error.processterminated) and waits up to 750µs for
the thread to finish. If your thread doesn't return within this window, a small amount of thread
metadata (environment, reference binary, thread struct) will leak. This is a best-effort cleanup
to avoid blocking the BEAM scheduler. In practice, if you call beam.yield() regularly and return
promptly when it returns an error, this leak will not occur.
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