├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dub.sdl ├── dub.selections.json ├── examples └── example.d ├── reggaefile.d ├── source └── fearless │ ├── concurrency.d │ ├── from.d │ ├── package.d │ └── sharing.d └── tests ├── ut ├── concurrency.d └── exclusive.d └── ut_main.d /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Dub Test 7 | strategy: 8 | matrix: 9 | os: 10 | - ubuntu-22.04 11 | - windows-2019 12 | - macos-11 13 | dc: 14 | - dmd-2.108.0 15 | - dmd-2.100.0 16 | - ldc-1.37.0 17 | - ldc-1.29.0 18 | 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Install D compiler 24 | uses: dlang-community/setup-dlang@v1.4.0 25 | with: 26 | compiler: ${{ matrix.dc }} 27 | 28 | - name: Test 29 | run: dub test -q --build=unittest-cov 30 | 31 | - name: Build binary 32 | run: dub build -q 33 | 34 | - uses: codecov/codecov-action@v3.1.4 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | docs/ 5 | *.exe 6 | *.o 7 | *.obj 8 | *.lst 9 | caring 10 | *.a 11 | *.lib 12 | *.exe 13 | *.dylib 14 | bin -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - d: dmd-2.084.0 7 | - d: dmd-2.083.1 8 | - d: dmd-2.082.1 9 | - d: ldc-1.13.0 10 | - d: ldc-1.12.0 11 | - d: ldc-1.11.0 12 | 13 | script: 14 | - dub test --build=unittest-cov --compiler=${DC} 15 | - dub run -c example 16 | 17 | after_success: 18 | - bash <(curl -s https://codecov.io/bash) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fearless 2 | 3 | [![Build Status](https://travis-ci.org/atilaneves/fearless.png?branch=master)](https://travis-ci.org/atilaneves/fearless) 4 | [![Coverage](https://codecov.io/gh/atilaneves/fearless/branch/master/graph/badge.svg)](https://codecov.io/gh/atilaneves/fearless) 5 | 6 | Safe concurrency in D 7 | 8 | This package implements `@safe` easy sharing of mutable data between threads without having 9 | to cast from shared and lock/unlock a mutex. It does so by using `scope` and 10 | [DIP1000](https://github.com/dlang/DIPs/blob/master/DIPs/DIP1000.md). It was inspired by 11 | Rust's [std::sync::Mutex](https://doc.rust-lang.org/1.21.0/std/sync/struct.Mutex.html). 12 | 13 | The main type is `Exclusive!T` which is safely shareable between 14 | threads even if T is not `immutable` or `shared`. To create one, call 15 | one of `gcExclusive` or `rcExclusive` with the parameters to the 16 | constructor to create a type T. Passing an already created T would not 17 | be safe since references to it or its internal data might exist 18 | elsewhere. 19 | 20 | As the names indicate, `gcExclusive` allocates on the GC heap, whereas `rcExclusive` uses 21 | `RefCounted` from [automem](https://github.com/atilaneves/automem). This works automatically 22 | if automem can be imported, which is always the case when automem is listed as a DUB 23 | dependency. 24 | 25 | To actually get access to the protected value, use `.lock()` (`borrow` 26 | exists as an alias) to get exclusive access for the current block of 27 | code. 28 | 29 | An example (notice that `main` is `@safe`): 30 | 31 | ```d 32 | import fearless; 33 | 34 | 35 | struct Foo { 36 | int i; 37 | } 38 | 39 | int* gEvilInt; 40 | 41 | 42 | void main() @safe { 43 | 44 | // create an instance of Exclusive!Foo allocated on the GC heap 45 | auto foo = gcExclusive!Foo(42); 46 | // from now the value inside `foo` can only be used by calling `lock` 47 | 48 | { 49 | int* oldIntPtr; // only here to demonstrate scopes, see below 50 | auto xfoo = foo.lock(); // get exclusive access to the data (this locks a mutex) 51 | 52 | safeWriteln("i: ", xfoo.i); 53 | xfoo.i = 1; 54 | safeWriteln("i: ", xfoo.i); 55 | 56 | // can't escape to a global 57 | static assert(!__traits(compiles, gEvilInt = &xfoo.i)); 58 | 59 | // ok to assign to a local that lives less 60 | int* intPtr; 61 | static assert(__traits(compiles, intPtr = &xfoo.i)); 62 | 63 | // not ok to assign to a local that lives longer 64 | static assert(!__traits(compiles, oldIntPtr = &xfoo.i)); 65 | } 66 | 67 | // Demonstrate sending to another thread and mutating 68 | auto tid = spawn(&func, thisTid); 69 | tid.send(foo); 70 | receiveOnly!Ended; 71 | safeWriteln("i: ", foo.lock.i); 72 | } 73 | 74 | struct Ended{} 75 | 76 | void func(Tid tid) @safe { 77 | receive( 78 | // ref Exclusive!Foo doesn't compile, use pointer instead 79 | (Exclusive!Foo* m) { 80 | auto xfoo = m.lock; 81 | xfoo.i++; 82 | }, 83 | ); 84 | 85 | tid.send(Ended()); 86 | } 87 | 88 | 89 | void safeWriteln(A...)(auto ref A args) { // for some reason the writelns here are all @system 90 | import std.stdio: writeln; 91 | import std.functional: forward; 92 | () @trusted { writeln(forward!args); }(); 93 | } 94 | 95 | ``` 96 | 97 | This program prints: 98 | 99 | ``` 100 | i: 42 101 | i: 1 102 | i: 2 103 | ``` 104 | 105 | Please consult the examples directory and/or unit tests for more. 106 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "fearless" 2 | description "Safe concurrency for D" 3 | authors "Atila Neves" 4 | copyright "Copyright © 2018, Atila Neves" 5 | license "boost" 6 | dflags "-preview=dip1000" 7 | 8 | 9 | configuration "default" { 10 | targetType "library" 11 | } 12 | 13 | 14 | configuration "example" { 15 | targetType "executable" 16 | targetName "fearless-example" 17 | targetPath "bin" 18 | sourceFiles "examples/example.d" 19 | } 20 | 21 | 22 | configuration "unittest" { 23 | targetType "executable" 24 | targetName "ut" 25 | targetPath "bin" 26 | importPaths "tests" 27 | sourcePaths "tests" 28 | mainSourceFile "tests/ut_main.d" 29 | dependency "unit-threaded" version="*" 30 | dependency "automem" version="*" 31 | dependency "test_allocator" version="*" 32 | } 33 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "automem": "0.6.9", 5 | "test_allocator": "0.3.4", 6 | "unit-threaded": "2.1.9" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/example.d: -------------------------------------------------------------------------------- 1 | import fearless; 2 | 3 | 4 | struct Foo { 5 | int i; 6 | } 7 | 8 | int* gEvilInt; 9 | 10 | 11 | void main() @safe { 12 | 13 | // create an instance of Exclusive!Foo allocated on the GC heap 14 | auto foo = gcExclusive!Foo(42); 15 | // from now the value inside `foo` can only be used by calling `lock` (a.k.a. `borrow`) 16 | 17 | { 18 | int* oldIntPtr; // only here to demostrate scopes, see below 19 | scope xfoo = foo.lock(); // get exclusive access to the data (this locks a mutex) 20 | 21 | safeWriteln("i: ", xfoo.i); 22 | xfoo.i = 1; 23 | safeWriteln("i: ", xfoo.i); 24 | 25 | // can't escape to a global 26 | static assert(!__traits(compiles, gEvilInt = &xfoo.i)); 27 | 28 | // ok to assign to a local that lives less 29 | int* intPtr; 30 | static assert(__traits(compiles, intPtr = &xfoo.i)); 31 | 32 | // not ok to assign to a local that lives longer 33 | static assert(!__traits(compiles, oldIntPtr = &xfoo.i)); 34 | } 35 | 36 | // Demonstrate sending to another thread and mutating 37 | auto tid = spawn(&func, thisTid); 38 | tid.send(foo); 39 | receiveOnly!Ended; 40 | safeWriteln("i: ", foo.lock.i); 41 | } 42 | 43 | struct Ended{} 44 | 45 | void func(Tid tid) @safe { 46 | receive( 47 | // ref Exclusive!Foo doesn't compile, use pointer instead 48 | (Exclusive!Foo* m) { 49 | auto xfoo = m.lock; 50 | xfoo.i++; 51 | }, 52 | ); 53 | 54 | tid.send(Ended()); 55 | } 56 | 57 | 58 | void safeWriteln(A...)(auto ref A args) { // for some reason the writelns here are all @system 59 | import std.stdio: writeln; 60 | import std.functional: forward; 61 | () @trusted { writeln(forward!args); }(); 62 | } 63 | -------------------------------------------------------------------------------- /reggaefile.d: -------------------------------------------------------------------------------- 1 | import reggae; 2 | 3 | enum testFlags = "-g -debug -w"; 4 | 5 | alias ut = dubTestTarget!(CompilerFlags(testFlags)); 6 | alias example = dubConfigurationTarget!(Configuration("example"), 7 | CompilerFlags(testFlags)); 8 | 9 | mixin build!(ut, example); 10 | -------------------------------------------------------------------------------- /source/fearless/concurrency.d: -------------------------------------------------------------------------------- 1 | /** 2 | Safe concurrency based on std.concurrency. 3 | */ 4 | module fearless.concurrency; 5 | 6 | 7 | public import std.concurrency: Tid, thisTid; 8 | 9 | 10 | auto spawn(F, A...)(F fn, auto ref A args) { 11 | import std.functional: forward; 12 | import std.concurrency: spawn_ = spawn; 13 | return () @trusted { return spawn_(fn, forward!args); }(); 14 | } 15 | 16 | 17 | /** 18 | Wraps std.concurrency.send to make sure it's not possible to send 19 | a fearless.sharing.Exclusive that is already locked to another thread. 20 | */ 21 | void send(A...)(Tid tid, auto ref A args) { 22 | 23 | import fearless.sharing: ExclusiveImpl; 24 | import std.functional: forward; 25 | import std.concurrency: send_ = send; 26 | import std.traits: isInstanceOf, isPointer, PointerTarget; 27 | 28 | static immutable alreadyLockedException = 29 | new Exception("Cannot send already locked Exclusive to another thread"); 30 | 31 | foreach(ref arg; args) { 32 | 33 | alias T = typeof(arg); 34 | 35 | static if(isPointer!T && isInstanceOf!(ExclusiveImpl, PointerTarget!T)) { 36 | if(arg.isLocked) 37 | throw alreadyLockedException; 38 | } 39 | } 40 | 41 | return () @trusted { send_(tid, forward!args); }(); 42 | } 43 | 44 | void receive(T...)(auto ref T ops) { 45 | import std.concurrency: receive_ = receive; 46 | import std.functional: forward; 47 | () @trusted { receive_(forward!ops); }(); 48 | } 49 | 50 | auto receiveOnly(A...)() { 51 | import std.concurrency: receiveOnly_ = receiveOnly; 52 | return () @trusted { return receiveOnly_!A(); }(); 53 | } 54 | -------------------------------------------------------------------------------- /source/fearless/from.d: -------------------------------------------------------------------------------- 1 | /** 2 | Utility to avoid top-level imports 3 | */ 4 | module fearless.from; 5 | 6 | /** 7 | Local imports everywhere. 8 | */ 9 | template from(string moduleName) { 10 | mixin("import from = " ~ moduleName ~ ";"); 11 | } 12 | -------------------------------------------------------------------------------- /source/fearless/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | Safe concurrency in D using DIP100 and scope. 3 | */ 4 | module fearless; 5 | 6 | 7 | public import fearless.sharing; 8 | public import fearless.concurrency; 9 | -------------------------------------------------------------------------------- /source/fearless/sharing.d: -------------------------------------------------------------------------------- 1 | /** 2 | D implementation of Rust's std::sync::Mutex 3 | */ 4 | module fearless.sharing; 5 | 6 | import fearless.from; 7 | 8 | /** 9 | A new exclusive reference to a payload of type T constructed from args. 10 | Allocated on the GC to make sure its lifetime is infinite and therefore 11 | safe to pass to other threads. 12 | */ 13 | auto gcExclusive(T, A...)(auto ref A args) { 14 | import std.functional: forward; 15 | return new Exclusive!T(forward!args); 16 | } 17 | 18 | /** 19 | A new exclusive reference to a payload. 20 | Allocated on the GC to make sure its lifetime is infinite and therefore 21 | safe to pass to other threads. 22 | 23 | This function sets the passed-in payload to payload.init to make sure 24 | that no references to it can be unsafely used. 25 | */ 26 | auto gcExclusive(T)(ref T payload) if(!from!"std.traits".hasUnsharedAliasing!T) { 27 | return new Exclusive!T(payload); 28 | } 29 | 30 | static if (is(typeof({import automem.ref_counted;}))) { 31 | 32 | /** 33 | A reference counted exclusive object (see above). 34 | */ 35 | auto rcExclusive(T, A...)(auto ref A args) { 36 | import automem.ref_counted: RefCounted; 37 | import std.functional: forward; 38 | return RefCounted!(Exclusive!T)(forward!args); 39 | } 40 | 41 | auto rcExclusive(T, Allocator, Args...)(Allocator allocator, auto ref Args args) 42 | if(from!"automem.traits".isAllocator!Allocator) 43 | { 44 | import automem.ref_counted: RefCounted; 45 | import std.traits: hasMember; 46 | 47 | enum isSingleton = hasMember!(Allocator, "instance"); 48 | 49 | static if(isSingleton) 50 | return RefCounted!(Exclusive!T, Allocator)(args); 51 | else 52 | return RefCounted!(Exclusive!T, Allocator)(allocator, args); 53 | } 54 | } 55 | 56 | 57 | alias Exclusive(T) = shared(ExclusiveImpl!T); 58 | 59 | 60 | /** 61 | Provides @safe exclusive access (via a mutex) to a payload of type T. 62 | Allows to share mutable data across threads safely. 63 | */ 64 | package struct ExclusiveImpl(T) { 65 | 66 | import std.traits: hasUnsharedAliasing, isAggregateType; 67 | 68 | import core.sync.mutex: Mutex; // TODO: make the mutex type a parameter 69 | 70 | private T _payload; 71 | private Mutex _mutex; 72 | private bool _locked; 73 | 74 | @disable this(this); 75 | 76 | /** 77 | The constructor is responsible for initialising the payload so that 78 | it's not possible to escape it. 79 | */ 80 | this(A...)(auto ref A args) shared { 81 | import std.functional: forward; 82 | this._payload = T(forward!args); 83 | init(); 84 | } 85 | 86 | static if(isAggregateType!T && !hasUnsharedAliasing!T) { 87 | /** 88 | Take a payload by ref in the case that it's safe, and set the original 89 | to T.init. 90 | */ 91 | private this(ref T payload) shared { 92 | import std.algorithm: move; 93 | import std.traits: Unqual; 94 | 95 | _payload = () @trusted { return cast(shared) move(payload); }(); 96 | payload = payload.init; 97 | 98 | init(); 99 | } 100 | } 101 | 102 | private void init() shared { 103 | this._mutex = new shared Mutex; 104 | } 105 | 106 | /** 107 | Whether or not the mutex is locked. 108 | */ 109 | bool isLocked() shared const { 110 | return _locked; 111 | } 112 | 113 | /** 114 | Obtain exclusive access to the payload. The mutex is locked and 115 | when the returned `Guard` object's lifetime is over the mutex 116 | is unloked. 117 | */ 118 | auto lock() shared { 119 | () @trusted { _mutex.lock_nothrow; }(); 120 | _locked = true; 121 | return Guard(&_payload, _mutex, &_locked); 122 | } 123 | 124 | alias borrow = lock; 125 | 126 | // non-static didn't work - weird error messages 127 | static struct Guard { 128 | 129 | private shared T* _payload; 130 | private shared Mutex _mutex; 131 | private shared bool* _locked; 132 | 133 | alias reference this; 134 | 135 | ref T reference() @trusted return scope { 136 | return *(cast(T*) _payload); 137 | } 138 | 139 | ~this() scope @trusted { 140 | *_locked = false; 141 | _mutex.unlock_nothrow(); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/ut/concurrency.d: -------------------------------------------------------------------------------- 1 | module ut.concurrency; 2 | 3 | 4 | import fearless.sharing; 5 | import fearless.concurrency; 6 | import unit_threaded; 7 | 8 | 9 | private struct Stop{} 10 | private struct Ended{} 11 | 12 | 13 | private void threadFunc(Tid tid) { 14 | import std.concurrency: receive, send; 15 | 16 | for(bool stop; !stop;) { 17 | 18 | receive( 19 | (Stop _) { 20 | stop = true; 21 | }, 22 | (Exclusive!int* m) { 23 | auto i = m.lock; 24 | ++i; 25 | }, 26 | ); 27 | } 28 | 29 | tid.send(Ended()); 30 | } 31 | 32 | @("send works") 33 | @safe unittest { 34 | auto tid = spawn(&threadFunc, thisTid); 35 | auto s = gcExclusive!int(42); 36 | tid.send(s); 37 | tid.send(Stop()); 38 | receiveOnly!Ended; 39 | } 40 | 41 | 42 | @("send fails when the mutex is already locked") 43 | @safe unittest { 44 | auto tid = spawn(&threadFunc, thisTid); 45 | auto s = gcExclusive!int(42); 46 | { 47 | auto xs = s.lock; 48 | tid.send(s).shouldThrowWithMessage( 49 | "Cannot send already locked Exclusive to another thread"); 50 | } 51 | 52 | tid.send(Stop()); 53 | receiveOnly!Ended; 54 | } 55 | -------------------------------------------------------------------------------- /tests/ut/exclusive.d: -------------------------------------------------------------------------------- 1 | module ut.exclusive; 2 | 3 | 4 | import fearless.sharing; 5 | import unit_threaded; 6 | 7 | 8 | @("GC exclusive int") 9 | @safe unittest { 10 | auto e = gcExclusive!int(42); 11 | } 12 | 13 | @("GC exclusive struct moved payload") 14 | @safe unittest { 15 | 16 | static struct Foo { 17 | int i; 18 | } 19 | 20 | auto foo = Foo(42); 21 | auto e = gcExclusive(foo); 22 | 23 | // should be reset to T.init 24 | foo.should == foo.init; 25 | 26 | { 27 | auto p = e.lock; 28 | p.reference.should == Foo(42); 29 | } 30 | } 31 | 32 | @("GC exclusive struct with indirection moved payload") 33 | @safe unittest { 34 | struct Struct { 35 | int[] ints; 36 | } 37 | 38 | auto s = Struct([1, 2, 3, 4]); 39 | auto i = s.ints; 40 | static assert(!__traits(compiles, gcExclusive(s))); 41 | } 42 | 43 | @("RC exclusive int default allocator") 44 | @system unittest { 45 | auto e = rcExclusive!int(42); 46 | } 47 | 48 | @("RC exclusive struct default allocator") 49 | @system unittest { 50 | static struct Foo { 51 | int i; 52 | double d; 53 | } 54 | 55 | auto e = rcExclusive!Foo(42, 33.3); 56 | { 57 | auto p = e.lock; 58 | p.reference.i.should == 42; 59 | p.reference.d.should ~ 33.3; 60 | } 61 | } 62 | 63 | @("RC exclusive struct mallocator") 64 | @system unittest { 65 | 66 | import std.experimental.allocator.mallocator: Mallocator; 67 | 68 | static struct Foo { 69 | int i; 70 | double d; 71 | } 72 | 73 | auto e = rcExclusive!Foo(Mallocator.instance, 42, 33.3); 74 | { 75 | auto p = e.lock; 76 | p.reference.i.should == 42; 77 | p.reference.d.should ~ 33.3; 78 | } 79 | } 80 | 81 | @("RC exclusive struct test allocator") 82 | @system unittest { 83 | 84 | import test_allocator: TestAllocator; 85 | 86 | static struct Foo { 87 | int i; 88 | double d; 89 | } 90 | 91 | auto allocator = TestAllocator(); 92 | auto e = rcExclusive!Foo(&allocator, 42, 33.3); 93 | { 94 | auto p = e.lock; 95 | p.reference.i.should == 42; 96 | p.reference.d.should ~ 33.3; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/ut_main.d: -------------------------------------------------------------------------------- 1 | import unit_threaded; 2 | 3 | int main(string[] args) { 4 | return args.runTests!( 5 | "ut.exclusive", 6 | "ut.concurrency", 7 | ); 8 | } 9 | --------------------------------------------------------------------------------