This example shows how to program a Lazy-evaluated thread-safe shared state object.
//
// Implementing Lazy-evaluated shared states
// Chapter10\Recipe1
//
// How it works
//
// The first example show why it is not safe to use the UnsafeState object with
// multiple accessing threads. We see that the Construct method was called
// several times, and different threads use different values, which is obviously
// not right. To fix this, we can use a lock when reading the value, and if it
// is not initialized, create it first. It will work, but using a lock with
// every read operation is not efficient. To avoid using locks every time, there
// is a traditional approach called the double-checked locking pattern. We check
// the value for the first time, and if is not null, we avoid unnecessary
// locking and just use the shared object. However, if it was not constructed
// yet, we use the lock and then check the value for the second time, because it
// could be initialized between our first check and the lock operation. If it is
// still not initialized, only then we compute the value. We can clearly see
// that this approach works with the second example---there is only one call to
// the Construct method, and the first-called thread defines the shared object
// state.
//
// Please note that if the lazy-evaluated object implementation is thread-safe,
// it does not automatically mean that all its properties are thread-safe as
// well.
//
// If you add, for example, an int public property to the ValueToAccess object,
// it will not be thread-safe; you still have to use interlocked constructs or
// locking to ensure thread safety.
//
// This pattern is very common, and that is why there are several classes in the
// Base Class Library to help us. First, we can use the
// LazyInitializer.EnsureInitialized method, which implements the double-checked
// locking pattern inside. However, the most comfortable option is to use the
// Lazy<T> class that allows us to have thread-safe Lazy-evaluated, shared
// state, out of the box. The next two examples show us that they are equivalent
// to the second one, and the program behaves the same. The only difference is
// that since LazyInitializer is a static class, we do not have to create a new
// instance of a class as we do in the case of Lazy<T>, and therefore the
// performance in the first case will be better in some scenarios.
//
// The last option is to avoid locking at all, if we do not care about the
// Construct method. If it is thread-safe and has no side effects and/or serious
// performance impacts, we can just run it several times but use only the first
// constructed value. The last example shows the described behavior, and we can
// achieve this result by using another LazyInitializer. EnsureInitialized
// method overload.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Chapter10.Recipe1
{
class Program
{
static void Main(string[] args)
{
var t = ProcessAsynchronously();
t.GetAwaiter().GetResult();
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
}
static async Task ProcessAsynchronously()
{
var unsafeState = new UnsafeState();
Task[] tasks = new Task[4];
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(unsafeState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
var firstState = new DoubleCheckedLocking();
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(firstState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
var secondState = new BCLDoubleChecked();
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(secondState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
var thirdState = new Lazy<ValueToAccess>(Compute);
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(thirdState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
var fourthState = new BCLThreadSafeFactory();
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(fourthState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
}
static void Worker(IHasValue state)
{
Console.WriteLine("Worker runs on thread id {0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("State value: {0}", state.Value.Text);
}
static void Worker(Lazy<ValueToAccess> state)
{
Console.WriteLine("Worker runs on thread id {0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("State value: {0}", state.Value.Text);
}
static ValueToAccess Compute()
{
Console.WriteLine("The value is being constructed on a thread id {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromSeconds(1));
return new ValueToAccess(string.Format("Constructed on thread id {0}", Thread.CurrentThread.ManagedThreadId));
}
class ValueToAccess
{
private readonly string _text;
public ValueToAccess(string text)
{
_text = text;
}
public string Text
{
get
{
return _text;
}
}
}
class UnsafeState : IHasValue
{
private ValueToAccess _value;
public ValueToAccess Value
{
get
{
if (_value == null)
{
_value = Compute();
}
return _value;
}
}
}
class DoubleCheckedLocking : IHasValue
{
private object _syncRoot = new object();
private volatile ValueToAccess _value;
public ValueToAccess Value
{
get
{
if (_value == null)
{
lock (_syncRoot)
{
if (_value == null)
{
_value = Compute();
}
}
}
return _value;
}
}
}
class BCLDoubleChecked : IHasValue
{
private object _syncRoot = new object();
private ValueToAccess _value;
private bool _initialized = false;
public ValueToAccess Value
{
get
{
return LazyInitializer.EnsureInitialized(
ref _value, ref _initialized, ref _syncRoot, Compute);
}
}
}
class BCLThreadSafeFactory : IHasValue
{
private ValueToAccess _value;
public ValueToAccess Value
{
get
{
return LazyInitializer.EnsureInitialized(ref _value, Compute);
}
}
}
interface IHasValue
{
ValueToAccess Value { get; }
}
}
}