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 Rust'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 one of 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 works automatically if automem can be imported, which is always the case when automem is listed as a DUB dependency.

To actually get access to the protected value, use .lock() (borrow exists as an alias) 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;  // only here to demonstrate scopes, see below
        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.