Skip to content

Instantly share code, notes, and snippets.

@bfriesen
Last active August 29, 2015 14:15
Show Gist options
  • Save bfriesen/f61862a4273f2ec7aa05 to your computer and use it in GitHub Desktop.
Save bfriesen/f61862a4273f2ec7aa05 to your computer and use it in GitHub Desktop.
Thread-safe, lock-free immutable collection transformation
<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