"The world is still short on languages that deal super elegantly and inherently and intuitively with concurrency" Mads Torgersen Lead Designer of C# (https://www.youtube.com/watch?v=Nuw3afaXLUc&t=4402s)
MemoizR is a library for .NET that brings the power of Dynamic Lazy Memoization and Declarative Structured Concurrency to your fingertips. It streamlines complex multi-threaded scenarios, making them easier to write, maintain, and reason about.
Inspired From Stephen Cleary — Asynchronous streams https://www.youtube.com/watch?v=-Tq4wLyen7Q&t=706s
| compared to | which is | MemoizR/Signals |
|---|---|---|
| IEnumerable | synchronous | asynchronous |
| Task | single value | multi value |
| Observable | push based | push-pull |
| IAsyncEnumerable | pull based | push-pull |
- Dynamic Lazy Memoization: Calculate values only when needed, avoiding unnecessary computations and optimizing performance.
- Declarative Structured Concurrency: Easily manage complex concurrency scenarios with straightforward configuration, effortless maintenance, robust error handling, and seamless cancellation.
- Dependency Graph:Automatically track dependencies between your data, ensuring that only necessary computations are performed.
- Automatic Synchronization: Work with shared state without the hassle of manual synchronization.
- Performance Optimization: Benefit from memoization for read-heavy scenarios and lazy evaluation for write-heavy scenarios. Inspiration
- Reactively and Solid: Dynamic lazy memoization concepts.
- VHDL: Synchronization mechanisms.
- ReactiveX: Reactive programming paradigms.
- Structured Concurrency: Principles for well-structured concurrent code. Special thanks to @mfp22 for the idea of signal operators!
MemoizR offers several advantages over traditional concurrency libraries:
No need to manage subscriptions manually; MemoizR automatically tracks and synchronizes dependencies. Implicit LinkTo: Dependencies are automatically linked based on your code's structure, simplifying data flow setup. Simplified Error Handling: Structured concurrency makes error handling more robust and easier to reason about.
// Setup
var f = new MemoFactory();
var v1 = f.CreateSignal(1);
var m1 = f.CreateMemoizR(async() => await v1.Get());
var m2 = f.CreateMemoizR(async() => await v1.Get() * 2);
var m3 = f.CreateMemoizR(async() => await m1.Get() + await m2.Get());
// Get Value manually
await m3.Get(); // Calculates m1 + 2 * m1 => (1 + 2 * 1) = 3
// Change
await Task.Run(async () => await v1.Set(2));
// Synchronization is handled by MemoizR
await m3.Get(); // Calculates m1 + 2 * m1 => (1 + 2 * 2) = 6
await m3.Get(); // No operation, result is still 6
await v1.Set(3); // Setting v1 does not trigger evaluation of the graph
await v1.Set(2); // Setting v1 does not trigger evaluation of the graph
await m3.Get(); // No operation, result is still 6 (because the last time the graph was evaluated, v1 was already 2)MemoizR can handle dynamic changes in the dependency graph:
var m3 = f.CreateMemoizR(async() => await v1.Get() ? await m1.Get() : await m2.Get());var f = new MemoFactory("DSC");
var child1 = f.CreateConcurrentMapReduce(
async c =>
{
await Task.Delay(3000, c.Token);
return 3;
});
// all tasks get canceled if one fails
var c1 = f.CreateConcurrentMapReduce(
async c =>
{
await child1.Get();
return 4;
});
var x = await c1.Get();A concurrent job can own resources. These resources will be disposed by the job after all its work is done.
var groupTask = f.CreateConcurrentMapReduce(async group =>
{
group.AddResource(myDisposableResource);
return await myDisposableResource.DoWorkAsync(group.Token);
});
await groupTask.Get(); // First, waits for all tasks to complete; then, disposes myDisposableResource.All exceptions raised by disposal of any resource are ignored.
var f = new MemoFactory();
var v1 = f.CreateSignal(1);
var m1 = f.CreateMemoizR(async() => await v1.Get());
var m2 = f.CreateMemoizR(async() => await v1.Get() * 2);
var r1 = f.CreateReaction(m1, m2, (val1, val2) => val1 + val2);With the MemoizR.Wpf package the whole dependency graph keeps evaluating on the thread pool;
only each reaction's action is dispatched to the WPF UI thread:
var f = new MemoFactory().AddWpfDispatcher(); // uses Application.Current.Dispatcher
var v1 = f.CreateSignal(1);
var m1 = f.CreateMemoizR(async () => await v1.Get() * 2); // computed on worker threads
// Dependencies are passed as separate parameters so they are evaluated in parallel on the
// thread pool; only the action below runs on the UI thread, with the already-computed values.
var r1 = f.CreateReaction(m1, v => viewModel.Value = v);
await v1.Set(5); // safe from any threadA specific Dispatcher can be supplied with f.AddWpfDispatcher(dispatcher). On other UI
stacks, register the UI SynchronizationContext directly from the UI thread with
f.AddSynchronizationContext(SynchronizationContext.Current!) (MemoizR.Reactive); reactions
built from the factory then follow the same contract: dependencies on the thread pool, action
on the registered context.
Try it out!https: Experiment with MemoizR online: https://dotnetfiddle.net/Widget/EWtptc
Example From: Khalid Abuhakmeh
Run the test suite with dotnet test. Thread interleavings of the locking code are explored
systematically with Microsoft Coyote; see
docs/Coyote.md for how to rewrite the assemblies and run the Coyote test.
