.NET Fiddle

CSharp · Console

/Program.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using System.Runtime.CompilerServices;

public static class Program
{
    public static void Main()
    {
        ThreadSafeData<(int, int, int, int, int, int, int, int, int)> data = new();
        data.Init();
        using CancellationTokenSource cts = new(1000);
        int writerSpinCount = 0;
        int readerSpinCount = 0;
        int readersCount = 4;
        Task<int> writer = new Task<int>(() =>
        {
            int iterations = 0;
            Random random = new();
            while (!cts.IsCancellationRequested)
            {
                iterations++;
                int value = random.Next();
                data.SetData((value, value, value, value, value, value, value, value, value));
                Thread.SpinWait(writerSpinCount);
            }
            return iterations;
        });
        Task<(int, int)>[] readers = Enumerable.Range(1, readersCount).Select(n => new Task<(int, int)>(() =>
        {
            int iterations = 0;
            int tornValues = 0;
            while (!cts.IsCancellationRequested)
            {
                iterations++;
                var d = data.GetData();
                if (d.Item1 != d.Item2 || d.Item1 != d.Item3 || d.Item1 != d.Item4 || d.Item1 != d.Item5 || d.Item1 != d.Item6 || d.Item1 != d.Item7 || d.Item1 != d.Item8 || d.Item1 != d.Item9)
                {
                    tornValues++;
                    if (tornValues == 1) Console.WriteLine($"Reader #{n}, First torn value: {d}");
                }
                Thread.SpinWait(readerSpinCount);
            }
            return (iterations, tornValues);
        })).ToArray();
        Console.WriteLine($"Type: {data.GetType().Name}, Tuple.Length: {((ITuple)data.GetData()).Length:#,0}");
        Console.WriteLine($"Writer (1) SpinCount: {writerSpinCount:#,0}");
        Console.WriteLine($"Reader ({readers.Length}) SpinCount: {readerSpinCount:#,0}");
        Console.WriteLine();
        ThreadPool.SetMinThreads(readers.Length + 1, 4);
        Task allTasks = Task.WhenAll(readers.Cast<Task>().Prepend(writer).ToArray());
        long mem0 = GC.GetTotalAllocatedBytes(true);
        Array.ForEach(readers, r => r.Start(TaskScheduler.Default));
        writer.RunSynchronously(TaskScheduler.Default);
        allTasks.Wait();
        long mem1 = GC.GetTotalAllocatedBytes(true);
        Console.WriteLine($"Writer iterations: {writer.Result:#,0}");
        Console.WriteLine($"Reader iterations: {String.Join(", ", readers.Select(r => $"{r.Result.Item1:#,0}"))} (Sum: {readers.Select(r => r.Result.Item1).Sum():#,0})");
        int sumTornValues = readers.Select(r => r.Result.Item2).Sum();
        if (sumTornValues > 0) Console.WriteLine($"Torn values: {String.Join(", ", readers.Select(r => $"{r.Result.Item2:#,0}"))} (Sum: {sumTornValues:#,0})");
        Console.WriteLine($"Allocated: {mem1 - mem0:#,0} bytes");
    }

    public struct ThreadSafeData<T>
    {
        private T[] dataArray;

        private int setterIndex;        
        private int lastSetIndex;

        public void Init()
        {
            dataArray = new T[2];
        }

        public void SetData(T data)
        {
            dataArray[setterIndex] = data;            
            lastSetIndex = setterIndex;

            // Convert 0 to 1, and 1 to 0
            setterIndex = lastSetIndex * - 1 + 1;
        }

        public T GetData()
        {            
            return dataArray[lastSetIndex];
        }
    }
}
;