diff --git a/src/LogicLooper/LogicLooperPool.Shared.cs b/src/LogicLooper/LogicLooperPool.Shared.cs index c016432..bc96230 100644 --- a/src/LogicLooper/LogicLooperPool.Shared.cs +++ b/src/LogicLooper/LogicLooperPool.Shared.cs @@ -4,10 +4,13 @@ namespace Cysharp.Threading; public sealed partial class LogicLooperPool { + private static readonly ILogicLooperPool _notInitializedSharedPool = new NotInitializedLogicLooperPool(); + private static ILogicLooperPool _shared = _notInitializedSharedPool; + /// /// Gets the shared pool of loopers. Requires to call method before use. /// - public static ILogicLooperPool Shared { get; private set; } = new NotInitializedLogicLooperPool(); + public static ILogicLooperPool Shared => Volatile.Read(ref _shared); /// /// Initializes the shared pool of loopers with specified options. @@ -16,18 +19,30 @@ public sealed partial class LogicLooperPool /// /// /// + /// The shared pool has already been initialized. public static void InitializeSharedPool(int targetFrameRate, int looperCount = 0, ILogicLooperPoolBalancer? balancer = null, ILogicLooperPoolLooperFactory? looperFactory = null) { + if (!ReferenceEquals(Volatile.Read(ref _shared), _notInitializedSharedPool)) + { + throw new InvalidOperationException("LogicLooperPool.Shared has already been initialized."); + } + if (looperCount == 0) { looperCount = Math.Max(1, Environment.ProcessorCount - 1); } - Shared = new LogicLooperPool( + var sharedPool = new LogicLooperPool( targetFrameRate, looperCount, balancer ?? RoundRobinLogicLooperPoolBalancer.Instance, looperFactory ?? DefaultLogicLooperPoolLooperFactory.Instance ); + + if (!ReferenceEquals(Interlocked.CompareExchange(ref _shared, sharedPool, _notInitializedSharedPool), _notInitializedSharedPool)) + { + sharedPool.Dispose(); + throw new InvalidOperationException("LogicLooperPool.Shared has already been initialized."); + } } } diff --git a/test/LogicLooper.Test/LogicLooperPoolSharedTest.cs b/test/LogicLooper.Test/LogicLooperPoolSharedTest.cs new file mode 100644 index 0000000..53e1efe --- /dev/null +++ b/test/LogicLooper.Test/LogicLooperPoolSharedTest.cs @@ -0,0 +1,97 @@ +using Cysharp.Threading; + +namespace LogicLooper.Test; + +public class LogicLooperPoolSharedTest +{ + [Fact] + public void InitializeSharedPool_CanOnlySucceedOnce() + { + const int concurrentCalls = 8; + using var factory = new TrackingLogicLooperFactory(concurrentCalls); + using var start = new ManualResetEventSlim(); + var results = new Exception[concurrentCalls]; + + var tasks = Enumerable.Range(0, concurrentCalls) + .Select(index => Task.Factory.StartNew( + () => + { + start.Wait(); + try + { + LogicLooperPool.InitializeSharedPool(60, 1, looperFactory: factory); + } + catch (Exception ex) + { + results[index] = ex; + } + }, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default)) + .ToArray(); + + start.Set(); + Task.WaitAll(tasks); + + try + { + Assert.Single(results, static x => x == null); + Assert.Equal(concurrentCalls - 1, results.Count(static x => x is InvalidOperationException)); + Assert.Equal(concurrentCalls, factory.CreatedCount); + Assert.Equal(concurrentCalls - 1, factory.DisposedCount); + + Assert.Throws(() => + LogicLooperPool.InitializeSharedPool(60, 1, looperFactory: factory)); + Assert.Equal(concurrentCalls, factory.CreatedCount); + } + finally + { + LogicLooperPool.Shared.Dispose(); + } + + Assert.Equal(concurrentCalls, factory.DisposedCount); + } + + private sealed class TrackingLogicLooperFactory(int expectedCreateCount) : ILogicLooperPoolLooperFactory, IDisposable + { + private readonly Barrier _createBarrier = new(expectedCreateCount); + private int _createdCount; + private int _disposedCount; + + public int CreatedCount => Volatile.Read(ref _createdCount); + public int DisposedCount => Volatile.Read(ref _disposedCount); + + public ILogicLooper Create(TimeSpan targetFrameTime) + { + Interlocked.Increment(ref _createdCount); + if (!_createBarrier.SignalAndWait(TimeSpan.FromSeconds(5))) + { + throw new TimeoutException("Timed out waiting for concurrent pool creation."); + } + return new TrackingLogicLooper(this); + } + + public void Dispose() => _createBarrier.Dispose(); + + private sealed class TrackingLogicLooper(TrackingLogicLooperFactory owner) : ILogicLooper + { + public int Id => 0; + public int ApproximatelyRunningActions => 0; + public TimeSpan LastProcessingDuration => TimeSpan.Zero; + public double TargetFrameRate => 60; + public long CurrentFrame => 0; + + public Task RegisterActionAsync(LogicLooperActionDelegate loopAction) => throw new NotSupportedException(); + public Task RegisterActionAsync(LogicLooperActionDelegate loopAction, LooperActionOptions options) => throw new NotSupportedException(); + public Task RegisterActionAsync(LogicLooperActionWithStateDelegate loopAction, TState state) => throw new NotSupportedException(); + public Task RegisterActionAsync(LogicLooperActionWithStateDelegate loopAction, TState state, LooperActionOptions options) => throw new NotSupportedException(); + public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction) => throw new NotSupportedException(); + public Task RegisterActionAsync(LogicLooperAsyncActionDelegate loopAction, LooperActionOptions options) => throw new NotSupportedException(); + public Task RegisterActionAsync(LogicLooperAsyncActionWithStateDelegate loopAction, TState state) => throw new NotSupportedException(); + public Task RegisterActionAsync(LogicLooperAsyncActionWithStateDelegate loopAction, TState state, LooperActionOptions options) => throw new NotSupportedException(); + public Task ShutdownAsync(TimeSpan shutdownDelay) => Task.CompletedTask; + public void Dispose() => Interlocked.Increment(ref owner._disposedCount); + } + } +}