Last active
August 29, 2015 14:15
-
-
Save bfriesen/f61862a4273f2ec7aa05 to your computer and use it in GitHub Desktop.
Thread-safe, lock-free immutable collection transformation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Query Kind="Program"> | |
<NuGetReference>Microsoft.Bcl.Immutable</NuGetReference> | |
<NuGetReference>NUnit</NuGetReference> | |
<Namespace>NUnit.Framework</Namespace> | |
<Namespace>System.Collections.Concurrent</Namespace> | |
<Namespace>System.Collections.Immutable</Namespace> | |
<Namespace>System.Threading.Tasks</Namespace> | |
</Query> | |
#define NONEST | |
void Main() | |
{ | |
// ATTENTION GIST USERS! Open this gist in LINPQad! | |
// | |
// From the menu in LINQPad, go to File > Open. (Or press <Ctrl> + O.) | |
// Then, enter this url: | |
// https://gist.githubusercontent.com/bfriesen/f61862a4273f2ec7aa05/raw/ | |
// We're going to be "filling" this list with multiple threads at the | |
// same time. Being immutable, that isn't exactly what happens. Instead, | |
// for each item, we'll replace its value with a new value that includes | |
// the new item. This means that it will have its value replaced over | |
// and over and over again. Each time safely and without locks. | |
var list = ImmutableList<int>.Empty; | |
const int threadCount = 40; | |
const int itemsPerThread = 25; | |
// The source of arbitrary data. Will be updated in a thread-safe manner | |
// using 'Interlocked.Increment'. | |
int i = 0; | |
// Create a list of threads. | |
var threads = | |
Enumerable.Range(0, threadCount) // Get 'threadCount' number of items. | |
.Select(_ => // For each item, | |
new Thread((ThreadStart)(() => // create a Thread. | |
{ | |
// Each thread adds 'itemsPerThread' number of items to the list. | |
foreach (var __ in Enumerable.Range(0, itemsPerThread)) | |
{ | |
// Get the next value in a thread-safe manner. | |
var nextValue = Interlocked.Increment(ref i); | |
// Add the value to the immutable list. | |
// This actually replaces the value of 'list' with a new | |
// immutable list that contains the elements of 'list' | |
// plus the new value. | |
Set.Immutable(ref list, currentList => currentList.Add(nextValue)); | |
} | |
}))) | |
.ToList(); // Eagerly evaluate so threads can start as quickly as possible. | |
// Start all the threads as quickly as possible. | |
Parallel.ForEach(threads, thread => thread.Start()); | |
// Wait for all the threads to finish. | |
foreach (var thread in threads) thread.Join(); | |
// Make sure the list has every number from 1 to threadCount * itemsPerThread. | |
// This throws an exception if that is not the case. | |
Assert.That(list, Is.EquivalentTo(Enumerable.Range(1, threadCount * itemsPerThread))); | |
// Print each item. Note that while some items will be out of order, | |
// all numbers are accounted for. | |
foreach (var item in list) Console.WriteLine(item); | |
} | |
public static class Set | |
{ | |
// Sets the value of 'actualValue' to the value returned by passing | |
// 'actualValue' to a call to 'transform'. Returns that new value. | |
// If 'condition' is provided, and if passing it 'actualValue' results | |
// in false, then 'transform' is not invoked and 'actualValue' is | |
// returned unmodified. | |
public static T Immutable<T>( | |
// A reference to a field or variable. | |
ref T actualValue, | |
// A function that takes the value of 'actualValue' and returns a new value. | |
Func<T, T> transform, | |
// A function that takes the value of 'actualValue' and determines whether | |
// 'actualValue' should be changed. Return true if 'actualValue' should | |
// change; return false if 'actualValue' should *not* change. | |
Func<T, bool> condition = null) where T : class | |
{ | |
if (transform == null) throw new ArgumentNullException("transform"); | |
// If 'condition' is not provided, use a function that always returns true. | |
condition = condition ?? (_ => true); | |
// These are needed by the while check of the do...while loop, so | |
// they must be declared outside the loop. | |
T initialValue, newValue; | |
do | |
{ | |
// Get a local copy of 'actualValue'. From here on out, use this | |
// local copy - never 'actualValue' itself. | |
initialValue = actualValue; | |
// Use our local copy to determine whether we should exit early. | |
if (!condition(initialValue)) // If false, exit early. | |
{ | |
// Don't modify anything, just return our local copy. | |
return initialValue; | |
} | |
// Use our local copy to get the new value. | |
newValue = transform(initialValue); | |
// 'Interlocked.CompareExchange' returns the initial value of | |
// 'actualValue', so if that return value is not the same as | |
// 'initialValue', it means that another thread has modified | |
// 'actualValue'. In that case, loop around and try again. | |
// However, if this thread does successfully change the value | |
// of 'actualValue', then we will break out of the loop. | |
} while (initialValue != Interlocked.CompareExchange(ref actualValue, newValue, initialValue)); | |
// We know that we successfully modified the value of 'actualValue' | |
// to be 'newValue', so return 'newValue'. | |
return newValue; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment