Skip to content

atilaneves/fearless

Repository files navigation

fearless

Build Status Coverage

Safe concurrency in D

This package implements @safe easy sharing of mutable data between threads without having to cast from shared and lock/unlock a mutex. It does so by using scope and DIP1000. It was inspired by Rusts's std::sync::Mutex.

The main type is Exclusive!T which is safely shareable between threads even if T is not immutable or shared. To create one, call of one gcExclusive or rcExclusive with the parameters to the constructor to create a type T. Passing an already created T would not be safe since references to it or its internal data might exist elsewhere.

As the names indicate, gcExclusive allocates on the GC heap, whereas rcExclusive uses RefCounted from automem. This is optional and only available at compile-time if the client code defines Have_automem, which is automatically done by dub if automem is listed as a dependency.

To actually get access to the protected value, use .lock() to get exclusive access for the current block of code.

An example (notice that main is @safe):

import fearless;


struct Foo {
    int i;
}

int* gEvilInt;


void main() @safe {

    // create an instance of Exclusive!Foo allocated on the GC heap
    auto foo = gcExclusive!Foo(42);
    // from now the value inside `foo` can only be used by calling `lock`

    {
        int* oldIntPtr;
        auto xfoo = foo.lock();  // get exclusive access to the data (this locks a mutex)

        safeWriteln("i: ", xfoo.i);
        xfoo.i = 1;
        safeWriteln("i: ", xfoo.i);

        // can't escape to a global
        static assert(!__traits(compiles, gEvilInt = &xfoo.i));

        // ok to assign to a local that lives less
        int* intPtr;
        static assert(__traits(compiles, intPtr = &xfoo.i));

        // not ok to assign to a local that lives longer
        static assert(!__traits(compiles, oldIntPtr = &xfoo.i));
    }

    // Demonstrate sending to another thread and mutating
    auto tid = spawn(&func, thisTid);
    tid.send(foo);
    receiveOnly!Ended;
    safeWriteln("i: ", foo.lock.i);
}

struct Ended{}

void func(Tid tid) @safe {
    receive(
        // ref Exclusive!Foo doesn't compile, use pointer instead
        (Exclusive!Foo* m) {
            auto xfoo = m.lock;
            xfoo.i++;
        },
    );

    tid.send(Ended());
}


void safeWriteln(A...)(auto ref A args) { // for some reason the writelns here are all @system
    import std.stdio: writeln;
    import std.functional: forward;
    () @trusted { writeln(forward!args); }();
}

This program prints:

i: 42
i: 1
i: 2

Please consult the examples directory and/or unit tests for more.