View Source Nif concurrency strategies

When 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");
  const e = @import("erl_nif");
  // this is a dirty_cpu nif.
  pub fn long_running(env: beam.env, pid: beam.pid) !void {
      // following code triggered when process is killed.
      defer {
        // note that the environment of the parent process is dead,
        // so we have to manually create a new environment and send
        // from it.

        const env2 = beam.alloc_env();
        const msg = beam.make(env2, .killed, .{});
        var pid2 = pid;
        _ = e.enif_send(null, &pid2, env2, msg.v);
        beam.free_env(env2);
      }

      while(true) {
          _ = try beam.send(env, pid, .unblock);
          try beam.yield(env);
      }
  }
  """

  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
end

queue 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(env: beam.env, pid: beam.pid) !void {
      // following code triggered when process is killed.
      // note that unlike dirty functions, the lifetime of 
      // env matches the lifetime of the function.

      defer {
        _ = beam.send(env, pid, .killed) catch {};
      }

      while(true) {
          _ = try beam.send(env, pid, .unblock);
          try beam.yield(env);
      }
  }
  """

  @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
end

Yielding

yielding nifs

Yielding nifs are not available in this release of Zigler

# module