Files
cneicy 1c921f5aec
Publish UPM package / publish (push) Failing after 1s
chore: initialize standalone UPM package
2026-08-26 02:50:18 +08:00

206 lines
7.4 KiB
C#

#nullable enable
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace ShrinkEventBus
{
internal interface IDotNetScheduler : IDisposable
{
bool IsOnSchedulerThread { get; }
bool TryPost(Action action);
ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken);
}
internal sealed class QueueFullException : InvalidOperationException { }
internal static class DotNetSchedulerFactory
{
public static IDotNetScheduler Create(string name, ShrinkBusOptions options)
{
return options.Scheduler switch
{
ShrinkBusSchedulerKind.MainThread => new SynchronizationContextScheduler(options),
ShrinkBusSchedulerKind.DedicatedThread => new DedicatedScheduler(name, options),
ShrinkBusSchedulerKind.TaskPool => new TaskPoolScheduler(options),
_ => new InlineScheduler()
};
}
}
internal sealed class InlineScheduler : IDotNetScheduler
{
public bool IsOnSchedulerThread => true;
public bool TryPost(Action action) { action(); return true; }
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return action();
}
public void Dispose() { }
}
internal sealed class SynchronizationContextScheduler : IDotNetScheduler
{
private readonly SynchronizationContext _context;
private readonly int _threadId;
private int _disposed;
public SynchronizationContextScheduler(ShrinkBusOptions options)
{
_context = SynchronizationContext.Current ?? throw new InvalidOperationException(
"MainThread scheduler requires a current SynchronizationContext in a pure .NET host.");
_threadId = Thread.CurrentThread.ManagedThreadId;
}
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == _threadId;
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0)
return false;
if (IsOnSchedulerThread)
action();
else
_context.Post(_ => action(), null);
return true;
}
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
if (IsOnSchedulerThread)
return action();
return new ValueTask(PostCoreAsync(action, cancellationToken));
}
private Task PostCoreAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_context.Post(async _ =>
{
try { cancellationToken.ThrowIfCancellationRequested(); await action(); completion.SetResult(true); }
catch (OperationCanceledException) { completion.SetCanceled(); }
catch (Exception ex) { completion.SetException(ex); }
}, null);
return completion.Task;
}
public void Dispose() => Interlocked.Exchange(ref _disposed, 1);
}
internal sealed class TaskPoolScheduler : IDotNetScheduler
{
private readonly SemaphoreSlim _concurrency;
private readonly int _capacity;
private int _pending;
private int _disposed;
public TaskPoolScheduler(ShrinkBusOptions options)
{
var concurrency = options.DispatchMode == ShrinkDispatchMode.Ordered ? 1 : options.MaxConcurrency;
_concurrency = new SemaphoreSlim(concurrency, concurrency);
_capacity = options.QueueCapacity;
}
public bool IsOnSchedulerThread => false;
public bool TryPost(Action action)
{
if (Volatile.Read(ref _disposed) != 0 || Interlocked.Increment(ref _pending) > _capacity)
{
Interlocked.Decrement(ref _pending);
return false;
}
_ = Task.Run(async () =>
{
await _concurrency.WaitAsync().ConfigureAwait(false);
try { action(); }
finally { _concurrency.Release(); Interlocked.Decrement(ref _pending); }
});
return true;
}
public async ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
if (Volatile.Read(ref _disposed) != 0 || Interlocked.Increment(ref _pending) > _capacity)
{
Interlocked.Decrement(ref _pending);
throw new QueueFullException();
}
await _concurrency.WaitAsync(cancellationToken).ConfigureAwait(false);
try { await action().ConfigureAwait(false); }
finally { _concurrency.Release(); Interlocked.Decrement(ref _pending); }
}
public void Dispose()
{
Interlocked.Exchange(ref _disposed, 1);
_concurrency.Dispose();
}
}
internal sealed class DedicatedScheduler : IDotNetScheduler
{
private sealed class Work
{
public Func<ValueTask> Action = null!;
public TaskCompletionSource<bool>? Completion;
}
private readonly BlockingCollection<Work> _queue;
private readonly Thread _thread;
private int _threadId;
private int _disposed;
public DedicatedScheduler(string name, ShrinkBusOptions options)
{
_queue = new BlockingCollection<Work>(options.QueueCapacity);
_thread = new Thread(Run) { IsBackground = true, Name = $"ShrinkBus:{name}" };
_thread.Start();
}
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == Volatile.Read(ref _threadId);
public bool TryPost(Action action)
{
if (IsOnSchedulerThread) { action(); return true; }
return Volatile.Read(ref _disposed) == 0 && _queue.TryAdd(new Work
{
Action = () => { action(); return default; }
});
}
public ValueTask PostAsync(Func<ValueTask> action, CancellationToken cancellationToken)
{
if (IsOnSchedulerThread)
return action();
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (Volatile.Read(ref _disposed) != 0 || !_queue.TryAdd(new Work
{ Action = action, Completion = completion }))
throw new QueueFullException();
return new ValueTask(completion.Task);
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;
_queue.CompleteAdding();
}
private void Run()
{
Volatile.Write(ref _threadId, Thread.CurrentThread.ManagedThreadId);
foreach (var work in _queue.GetConsumingEnumerable())
{
try { work.Action().AsTask().GetAwaiter().GetResult(); work.Completion?.SetResult(true); }
catch (OperationCanceledException) { work.Completion?.SetCanceled(); }
catch (Exception ex) { work.Completion?.SetException(ex); }
}
_queue.Dispose();
}
}
}