I'm trying to assess what is the fastest solution for sharing state across a single-writer, single-reader scenario, where the reader just consumes the latest value of a state variable assigned by the writer. The shared state can be of any managed type (i.e. reference or value types).
Ideally the synchronization solution would work as fast as the naive non-synchronized solution as possible, since the method will be used for both single-threaded and multi-threaded scenarios potentially thousands of times.
The order of read/writes shouldn't matter, as long as reader receives the latest value within some time frame (i.e. reader will only ever read, never modify, so update timing doesn't matter as long as it doesn't receive a future value before an older value...)
Naive solution, no locking:
var memory = default(int);
var reader = Task.Run(() =>
{
while (true)
{
func(memory);
}
});
var writer = Task.Run(() =>
{
while (true)
{
memory = DateTime.Now.Ticks;
}
});
What are really the issues with the naive solution? I've come up with these so far:
- No guarantee reader sees the latest value (no memory barrier/volatile)
- Values consumed by reader may be invalid if the type of the shared variable is not a primitive type or reference type (e.g. composite value types).
The straightforward solution would be locking:
var gate = new object();
var memory = default(int);
var reader = Task.Run(() =>
{
while (true)
{
int local;
lock(gate) { local = memory; }
func(local);
}
});
var writer = Task.Run(() =>
{
while (true)
{
lock(gate)
{
memory = DateTime.Now.Ticks;
}
}
});
This of course works, but incurs the price of locking (~50ns) in the single-threaded case, and of course the price of context-switching/contention in the multi-threaded case.
This is totally negligible for most scenarios, but it is significant in my case since the method will be used across the board on potentially thousands of loops which need to be as timely as possible running tens of thousands of times per second.
The last solution I thought about is using immutable state closures to read the shared state:
Func<int> memory = () => default(int);
var reader = Task.Run(() =>
{
while (true)
{
func(memory());
}
});
var writer = Task.Run(() =>
{
while (true)
{
var state = DateTime.Now.Ticks;
memory = () => state;
}
});
Now what would be the potential issues with this? My own performance benchmarks report ~10ns for this solution vs locking in the single-threaded case. It seems like a nice gain, but some considerations include:
- Still no memory barrier/volatile so reader is not guaranteed to see the latest closure (how common is this actually? would be good to know...)
- The atomicity problem is solved: since closure is a reference type, reading/writing has guaranteed atomicity according to the standard
- Boxing costs: basically using a closure implies allocating memory on the heap somehow, which is happening on every iteration. Unclear what the costs of this really are but it seems to be faster than locking...
Is there anything else I'm missing? Do you usually consider this use for closures instead of locks in your programs? Would also be great to know other possible fast solutions for single-reader/single-writer shared state.