Initial commit
This commit is contained in:
135
srcs/RobotNet10/Commons/RobotNet10.Common/WatchThreadAsync.cs
Normal file
135
srcs/RobotNet10/Commons/RobotNet10.Common/WatchThreadAsync.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet10.Common;
|
||||
|
||||
public class WatchThreadAsync<T>(int Interval, Func<Task> Callback, ILogger<T>? Logger, ThreadPriority Priority = ThreadPriority.Highest) : IDisposable where T : class
|
||||
{
|
||||
public bool Disposed;
|
||||
|
||||
private Thread? Thread;
|
||||
private CancellationTokenSource? ThreadCts;
|
||||
|
||||
private long NextDueTime;
|
||||
private readonly Lock Lock = new();
|
||||
private async Task Handler(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool shouldRun = false;
|
||||
lock (Lock)
|
||||
{
|
||||
if (Disposed) return;
|
||||
long now = GetCurrentTimeMs();
|
||||
if (now >= NextDueTime)
|
||||
{
|
||||
shouldRun = true;
|
||||
long scheduledTime = NextDueTime;
|
||||
NextDueTime += Interval;
|
||||
|
||||
if (now - scheduledTime > Interval / 2)
|
||||
{
|
||||
NextDueTime = now + Interval;
|
||||
if (Logger is not null && Logger.IsEnabled(LogLevel.Warning)) Logger.LogWarning("WatchThreadAsync Warning: Elapsed time {peak}ms exceeds interval {Interval}ms.", now - scheduledTime + Interval, Interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRun)
|
||||
{
|
||||
try { await Callback.Invoke(); }
|
||||
catch (Exception ex) { if (Logger is not null && Logger.IsEnabled(LogLevel.Error)) Logger.LogError("Callback error: {ex}", ex.Message); }
|
||||
}
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
if (Disposed) return;
|
||||
long now = GetCurrentTimeMs();
|
||||
long delay = NextDueTime - now;
|
||||
if (delay < 0) delay = 0;
|
||||
Thread.Sleep((int)delay);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (Logger is not null && Logger.IsEnabled(LogLevel.Error)) Logger.LogError("WatchThreadAsync Error: {ex}", ex.Message);
|
||||
Thread.Sleep(Interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long GetCurrentTimeMs()
|
||||
{
|
||||
return Stopwatch.GetTimestamp() * 1000 / Stopwatch.Frequency;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
if (!Disposed)
|
||||
{
|
||||
if (Thread?.IsAlive == true) return;
|
||||
|
||||
NextDueTime = GetCurrentTimeMs() + Interval;
|
||||
ThreadCts = new CancellationTokenSource();
|
||||
Thread = new Thread(async () => await Handler(ThreadCts.Token))
|
||||
{
|
||||
Priority = Priority,
|
||||
IsBackground = false,
|
||||
Name = $"WatchThreadAsync-{typeof(T).Name}"
|
||||
};
|
||||
Thread.Start();
|
||||
}
|
||||
else throw new ObjectDisposedException(nameof(WatchThreadAsync<>));
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
Thread? threadToJoin;
|
||||
lock (Lock)
|
||||
{
|
||||
if (Thread == null) return;
|
||||
ThreadCts?.Cancel();
|
||||
threadToJoin = Thread;
|
||||
}
|
||||
|
||||
// If Stop() is called from within the Callback (same thread), skip Join to avoid deadlock
|
||||
if (threadToJoin != null && threadToJoin != Thread.CurrentThread)
|
||||
{
|
||||
if (!threadToJoin.Join(TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
Logger?.LogWarning("Thread did not stop gracefully");
|
||||
}
|
||||
}
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
ThreadCts?.Dispose();
|
||||
ThreadCts = null;
|
||||
Thread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (Disposed) return;
|
||||
|
||||
Disposed = true;
|
||||
if (disposing) Stop();
|
||||
}
|
||||
|
||||
~WatchThreadAsync()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user