diff --git a/src/SquidStd.Core/Interfaces/Threading/IEventLoopService.cs b/src/SquidStd.Core/Interfaces/Threading/IEventLoopService.cs index b6d5af8..19e01b6 100644 --- a/src/SquidStd.Core/Interfaces/Threading/IEventLoopService.cs +++ b/src/SquidStd.Core/Interfaces/Threading/IEventLoopService.cs @@ -15,4 +15,7 @@ public interface IEventLoopService /// Worst observed tick elapsed time in milliseconds. double MaxTickMs { get; } + + /// True when the calling thread is the dedicated event-loop thread. + bool IsOnLoopThread { get; } } diff --git a/src/SquidStd.Services.Core/Services/EventLoop/EventLoopService.cs b/src/SquidStd.Services.Core/Services/EventLoop/EventLoopService.cs index d2e9a03..9edf5c5 100644 --- a/src/SquidStd.Services.Core/Services/EventLoop/EventLoopService.cs +++ b/src/SquidStd.Services.Core/Services/EventLoop/EventLoopService.cs @@ -28,6 +28,7 @@ public sealed class EventLoopService : IEventLoopService, ISquidStdService, IMet private double _maxTickMs; private Thread? _thread; private long _tickCount; + private volatile int _loopThreadId = -1; /// public long TickCount => Interlocked.Read(ref _tickCount); @@ -56,6 +57,9 @@ public double MaxTickMs } } + /// + public bool IsOnLoopThread => _loopThreadId == Environment.CurrentManagedThreadId; + /// public string ProviderName => "eventloop"; @@ -85,6 +89,9 @@ public ValueTask StopAsync(CancellationToken cancellationToken = default) _cts.Cancel(); _thread?.Join(TimeSpan.FromSeconds(5)); + // Reset so a recycled managed thread id can't later report as the loop thread. + _loopThreadId = -1; + return ValueTask.CompletedTask; } @@ -136,6 +143,8 @@ internal int Tick() private void RunLoop() { + _loopThreadId = Environment.CurrentManagedThreadId; + while (!_cts.IsCancellationRequested) { var work = Tick(); diff --git a/tests/SquidStd.Tests/EventLoop/EventLoopServiceTests.cs b/tests/SquidStd.Tests/EventLoop/EventLoopServiceTests.cs index 784e222..dfda3a2 100644 --- a/tests/SquidStd.Tests/EventLoop/EventLoopServiceTests.cs +++ b/tests/SquidStd.Tests/EventLoop/EventLoopServiceTests.cs @@ -80,4 +80,30 @@ public async Task StartStop_ExecutesPostedWork() Assert.True(fired); } + + [Fact] + public async Task IsOnLoopThread_TrueInsidePostedWork_FalseOutside() + { + var dispatcher = new MainThreadDispatcherService(); + var timer = new FakeTimerService(); + using var loop = new EventLoopService(dispatcher, timer, new EventLoopConfig()); + + Assert.False(loop.IsOnLoopThread); + + bool onLoopInside = false; + var signal = new ManualResetEventSlim(false); + dispatcher.Post(() => + { + onLoopInside = loop.IsOnLoopThread; + signal.Set(); + } + ); + + await loop.StartAsync(); + signal.Wait(TimeSpan.FromSeconds(2)); + await loop.StopAsync(); + + Assert.True(onLoopInside); + Assert.False(loop.IsOnLoopThread); + } }