diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/CatchScheduler.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/CatchScheduler.cs index 327fe64de2..e8d534dfc7 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/CatchScheduler.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/CatchScheduler.cs @@ -76,16 +76,18 @@ public CatchSchedulerLongRunning(ISchedulerLongRunning scheduler, Func(TState state, Action action) { - return _scheduler.ScheduleLongRunning(state, (state_, cancel) => - { - try - { - action(state_, cancel); - } - catch (TException exception) when (_handler(exception)) + return _scheduler.ScheduleLongRunning( + (scheduler: this, action, state), + (tuple, cancel) => { - } - }); + try + { + tuple.action(tuple.state, cancel); + } + catch (TException exception) when (tuple.scheduler._handler(exception)) + { + } + }); } } diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/ConcurrencyAbstractionLayerImpl.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/ConcurrencyAbstractionLayerImpl.cs index 8af1f261e6..2c1803bbe8 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/ConcurrencyAbstractionLayerImpl.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/ConcurrencyAbstractionLayerImpl.cs @@ -16,6 +16,18 @@ namespace System.Reactive.Concurrency // internal class /*Default*/ConcurrencyAbstractionLayerImpl : IConcurrencyAbstractionLayer { + private sealed class WorkItem + { + public WorkItem(Action action, object state) + { + this.Action = action; + this.State = state; + } + + public Action Action { get; } + public object State { get; } + } + public IDisposable StartTimer(Action action, object state, TimeSpan dueTime) => new Timer(action, state, Normalize(dueTime)); public IDisposable StartPeriodicTimer(Action action, TimeSpan period) @@ -39,7 +51,13 @@ public IDisposable StartPeriodicTimer(Action action, TimeSpan period) public IDisposable QueueUserWorkItem(Action action, object state) { - System.Threading.ThreadPool.QueueUserWorkItem(_ => action(_), state); + System.Threading.ThreadPool.QueueUserWorkItem(itemObject => + { + var item = (WorkItem)itemObject; + + item.Action(item.State); + }, new WorkItem(action, state)); + return Disposable.Empty; } @@ -51,10 +69,12 @@ public IDisposable QueueUserWorkItem(Action action, object state) public void StartThread(Action action, object state) { - new Thread(() => + new Thread(itemObject => { - action(state); - }) { IsBackground = true }.Start(); + var item = (WorkItem)itemObject; + + item.Action(item.State); + }) { IsBackground = true }.Start(new WorkItem(action, state)); } private static TimeSpan Normalize(TimeSpan dueTime) => dueTime < TimeSpan.Zero ? TimeSpan.Zero : dueTime; @@ -132,11 +152,13 @@ public void StartThread(Action action, object state) private sealed class Timer : IDisposable { + private object _state; private Action _action; private volatile System.Threading.Timer _timer; public Timer(Action action, object state, TimeSpan dueTime) { + _state = state; _action = action; // Don't want the spin wait in Tick to get stuck if this thread gets aborted. @@ -144,23 +166,25 @@ public Timer(Action action, object state, TimeSpan dueTime) finally { // - // Rooting of the timer happens through the this.Tick delegate's target object, + // Rooting of the timer happens through the Timer's state // which is the current instance and has a field to store the Timer instance. // - _timer = new System.Threading.Timer(Tick, state, dueTime, TimeSpan.FromMilliseconds(System.Threading.Timeout.Infinite)); + _timer = new System.Threading.Timer(_ => Tick(_), this, dueTime, TimeSpan.FromMilliseconds(System.Threading.Timeout.Infinite)); } } - private void Tick(object state) + private static void Tick(object state) { + var timer = (Timer) state; + try { - _action(state); + timer._action(timer._state); } finally { - SpinWait.SpinUntil(IsTimerAssigned); - Dispose(); + SpinWait.SpinUntil(timer.IsTimerAssigned); + timer.Dispose(); } } @@ -173,6 +197,7 @@ public void Dispose() { _action = Stubs.Ignore; _timer = TimerStubs.Never; + _state = null; timer.Dispose(); } @@ -189,13 +214,18 @@ public PeriodicTimer(Action action, TimeSpan period) _action = action; // - // Rooting of the timer happens through the this.Tick delegate's target object, + // Rooting of the timer happens through the timer's state // which is the current instance and has a field to store the Timer instance. // - _timer = new System.Threading.Timer(Tick, null, period, period); + _timer = new System.Threading.Timer(_ => Tick(_), this, period, period); } - private void Tick(object state) => _action(); + private static void Tick(object state) + { + var timer = (PeriodicTimer)state; + + timer._action(); + } public void Dispose() { @@ -219,19 +249,21 @@ public FastPeriodicTimer(Action action) { _action = action; - new System.Threading.Thread(Loop) + new System.Threading.Thread(_ => Loop(_)) { Name = "Rx-FastPeriodicTimer", IsBackground = true } - .Start(); + .Start(this); } - private void Loop() + private static void Loop(object threadParam) { - while (!disposed) + var timer = (FastPeriodicTimer)threadParam; + + while (!timer.disposed) { - _action(); + timer._action(); } } diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/DefaultScheduler.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/DefaultScheduler.cs index 551ca8bc30..c79c650a51 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/DefaultScheduler.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/DefaultScheduler.cs @@ -12,6 +12,43 @@ namespace System.Reactive.Concurrency /// Singleton instance of this type exposed through this static property. public sealed class DefaultScheduler : LocalScheduler, ISchedulerPeriodic { + private sealed class UserWorkItem : IDisposable + { + private IDisposable _cancelRunDisposable; + private IDisposable _cancelQueueDisposable; + + private readonly TState _state; + private readonly IScheduler _scheduler; + private readonly Func _action; + + public UserWorkItem(IScheduler scheduler, TState state, Func action) + { + _state = state; + _action = action; + _scheduler = scheduler; + } + + public void Run() + { + if (!Disposable.GetIsDisposed(ref _cancelRunDisposable)) + { + Disposable.TrySetSingle(ref _cancelRunDisposable, _action(_scheduler, _state)); + } + } + + public IDisposable CancelQueueDisposable + { + get => Disposable.GetValue(ref _cancelQueueDisposable); + set => Disposable.TrySetSingle(ref _cancelQueueDisposable, value); + } + + public void Dispose() + { + Disposable.TryDispose(ref _cancelQueueDisposable); + Disposable.TryDispose(ref _cancelRunDisposable); + } + } + private static readonly Lazy s_instance = new Lazy(() => new DefaultScheduler()); private static IConcurrencyAbstractionLayer s_cal = ConcurrencyAbstractionLayer.Current; @@ -37,20 +74,13 @@ public override IDisposable Schedule(TState state, Func(this, state, action); - var cancel = s_cal.QueueUserWorkItem(_ => - { - if (!d.IsDisposed) - { - d.Disposable = action(this, state); - } - }, null); + workItem.CancelQueueDisposable = s_cal.QueueUserWorkItem( + closureWorkItem => ((UserWorkItem)closureWorkItem).Run(), + workItem); - return StableCompositeDisposable.Create( - d, - cancel - ); + return workItem; } /// @@ -71,20 +101,14 @@ public override IDisposable Schedule(TState state, TimeSpan dueTime, Fun if (dt.Ticks == 0) return Schedule(state, action); - var d = new SingleAssignmentDisposable(); + var workItem = new UserWorkItem(this, state, action); - var cancel = s_cal.StartTimer(_ => - { - if (!d.IsDisposed) - { - d.Disposable = action(this, state); - } - }, null, dt); + workItem.CancelQueueDisposable = s_cal.StartTimer( + closureWorkItem => ((UserWorkItem)closureWorkItem).Run(), + workItem, + dt); - return StableCompositeDisposable.Create( - d, - cancel - ); + return workItem; } /// diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Async.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Async.cs index e063f4bc4a..475ef2af26 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Async.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Async.cs @@ -159,7 +159,7 @@ public static IDisposable ScheduleAsync(this IScheduler scheduler, Func action(self, ct)); + return ScheduleAsync_(scheduler, action, (self, closureAction, ct) => closureAction(self, ct)); } /// @@ -176,7 +176,7 @@ public static IDisposable ScheduleAsync(this IScheduler scheduler, Func action(self, ct)); + return ScheduleAsync_(scheduler, action, (self, closureAction, ct) => closureAction(self, ct)); } /// @@ -234,7 +234,7 @@ public static IDisposable ScheduleAsync(this IScheduler scheduler, TimeSpan dueT if (action == null) throw new ArgumentNullException(nameof(action)); - return ScheduleAsync_(scheduler, default(object), dueTime, (self, o, ct) => action(self, ct)); + return ScheduleAsync_(scheduler, action, dueTime, (self, closureAction, ct) => closureAction(self, ct)); } /// @@ -252,7 +252,7 @@ public static IDisposable ScheduleAsync(this IScheduler scheduler, TimeSpan dueT if (action == null) throw new ArgumentNullException(nameof(action)); - return ScheduleAsync_(scheduler, default(object), dueTime, (self, o, ct) => action(self, ct)); + return ScheduleAsync_(scheduler, action, dueTime, (self, closureAction, ct) => closureAction(self, ct)); } /// @@ -310,7 +310,7 @@ public static IDisposable ScheduleAsync(this IScheduler scheduler, DateTimeOffse if (action == null) throw new ArgumentNullException(nameof(action)); - return ScheduleAsync_(scheduler, default(object), dueTime, (self, o, ct) => action(self, ct)); + return ScheduleAsync_(scheduler, action, dueTime, (self, closureAction, ct) => closureAction(self, ct)); } /// @@ -328,37 +328,37 @@ public static IDisposable ScheduleAsync(this IScheduler scheduler, DateTimeOffse if (action == null) throw new ArgumentNullException(nameof(action)); - return ScheduleAsync_(scheduler, default(object), dueTime, (self, o, ct) => action(self, ct)); + return ScheduleAsync_(scheduler, action, dueTime, (self, closureAction, ct) => closureAction(self, ct)); } private static IDisposable ScheduleAsync_(IScheduler scheduler, TState state, Func action) { - return scheduler.Schedule(state, (self, s) => InvokeAsync(self, s, action)); + return scheduler.Schedule((state, action), (self, t) => InvokeAsync(self, t.state, t.action)); } private static IDisposable ScheduleAsync_(IScheduler scheduler, TState state, Func> action) { - return scheduler.Schedule(state, (self, s) => InvokeAsync(self, s, action)); + return scheduler.Schedule((state, action), (self, t) => InvokeAsync(self, t.state, t.action)); } private static IDisposable ScheduleAsync_(IScheduler scheduler, TState state, TimeSpan dueTime, Func action) { - return scheduler.Schedule(state, dueTime, (self, s) => InvokeAsync(self, s, action)); + return scheduler.Schedule((state, action), dueTime, (self, t) => InvokeAsync(self, t.state, t.action)); } private static IDisposable ScheduleAsync_(IScheduler scheduler, TState state, TimeSpan dueTime, Func> action) { - return scheduler.Schedule(state, dueTime, (self, s) => InvokeAsync(self, s, action)); + return scheduler.Schedule((state, action), dueTime, (self, t) => InvokeAsync(self, t.state, t.action)); } private static IDisposable ScheduleAsync_(IScheduler scheduler, TState state, DateTimeOffset dueTime, Func action) { - return scheduler.Schedule(state, dueTime, (self, s) => InvokeAsync(self, s, action)); + return scheduler.Schedule((state, action), dueTime, (self, t) => InvokeAsync(self, t.state, t.action)); } private static IDisposable ScheduleAsync_(IScheduler scheduler, TState state, DateTimeOffset dueTime, Func> action) { - return scheduler.Schedule(state, dueTime, (self, s) => InvokeAsync(self, s, action)); + return scheduler.Schedule((state, action), dueTime, (self, t) => InvokeAsync(self, t.state, t.action)); } private static IDisposable InvokeAsync(IScheduler self, TState s, Func> action) @@ -384,7 +384,7 @@ private static IDisposable InvokeAsync(IScheduler self, TState s, Func(IScheduler self, TState s, Func action) { - return InvokeAsync(self, s, (self_, state, ct) => action(self_, state, ct).ContinueWith(_ => Disposable.Empty)); + return InvokeAsync(self, (action, state: s), (self_, t, ct) => t.action(self_, t.state, ct).ContinueWith(_ => Disposable.Empty)); } private static CancellationToken GetCancellationToken(this IScheduler scheduler) diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Recursive.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Recursive.cs index e98d9c69fd..dc7f569101 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Recursive.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Recursive.cs @@ -41,18 +41,16 @@ public static IDisposable Schedule(this IScheduler scheduler, TState sta if (action == null) throw new ArgumentNullException(nameof(action)); - return scheduler.Schedule(new Pair>> { First = state, Second = action }, InvokeRec1); + return scheduler.Schedule((state, action), (s, p) => InvokeRec1(s, p)); } - private static IDisposable InvokeRec1(IScheduler scheduler, Pair>> pair) + private static IDisposable InvokeRec1(IScheduler scheduler, (TState state, Action> action) tuple) { var group = new CompositeDisposable(1); var gate = new object(); - var state = pair.First; - var action = pair.Second; Action recursiveAction = null; - recursiveAction = state1 => action(state1, state2 => + recursiveAction = state1 => tuple.action(state1, state2 => { var isAdded = false; var isDone = false; @@ -84,7 +82,7 @@ private static IDisposable InvokeRec1(IScheduler scheduler, Pair(this IScheduler scheduler, TState sta if (action == null) throw new ArgumentNullException(nameof(action)); - return scheduler.Schedule(new Pair>> { First = state, Second = action }, dueTime, InvokeRec2); + return scheduler.Schedule((state, action), dueTime, (s, p) => InvokeRec2(s, p)); } - private static IDisposable InvokeRec2(IScheduler scheduler, Pair>> pair) + private static IDisposable InvokeRec2(IScheduler scheduler, (TState state, Action> action) tuple) { var group = new CompositeDisposable(1); var gate = new object(); - var state = pair.First; - var action = pair.Second; Action recursiveAction = null; - recursiveAction = state1 => action(state1, (state2, dueTime1) => + recursiveAction = state1 => tuple.action(state1, (state2, dueTime1) => { var isAdded = false; var isDone = false; @@ -167,7 +163,7 @@ private static IDisposable InvokeRec2(IScheduler scheduler, Pair(this IScheduler scheduler, TState sta if (action == null) throw new ArgumentNullException(nameof(action)); - return scheduler.Schedule(new Pair>> { First = state, Second = action }, dueTime, InvokeRec3); + return scheduler.Schedule((state, action), dueTime, (s, p) => InvokeRec3(s, p)); } - private static IDisposable InvokeRec3(IScheduler scheduler, Pair>> pair) + private static IDisposable InvokeRec3(IScheduler scheduler, (TState state, Action> action) tuple) { var group = new CompositeDisposable(1); var gate = new object(); - var state = pair.First; - var action = pair.Second; Action recursiveAction = null; - recursiveAction = state1 => action(state1, (state2, dueTime1) => + recursiveAction = state1 => tuple.action(state1, (state2, dueTime1) => { var isAdded = false; var isDone = false; @@ -250,18 +244,9 @@ private static IDisposable InvokeRec3(IScheduler scheduler, Pair - { - public T1 First; - public T2 Second; - } } } diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Services.Emulation.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Services.Emulation.cs index 85c519f409..e39c67984a 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Services.Emulation.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Services.Emulation.cs @@ -60,7 +60,7 @@ public static IDisposable SchedulePeriodic(this IScheduler scheduler, TS if (action == null) throw new ArgumentNullException(nameof(action)); - return SchedulePeriodic_(scheduler, state, period, state_ => { action(state_); return state_; }); + return SchedulePeriodic_(scheduler, (state, action), period, t => { t.action(t.state); return t; }); } /// diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Simple.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Simple.cs index 9edd8fceb2..2c2ffb2e61 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Simple.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/Scheduler.Simple.cs @@ -22,7 +22,14 @@ public static IDisposable Schedule(this IScheduler scheduler, Action action) if (action == null) throw new ArgumentNullException(nameof(action)); - return scheduler.Schedule(action, Invoke); + // Surprisingly, passing the method group of Invoke will create a fresh + // delegate each an every time, although it's static, while an anonymous + // lambda without the need of a closure will be cached. + // Once Roslyn supports caching delegates for method groups, + // the anonymous lambda can be replaced by the method group again. Until then, + // to avoid the repetition of code, the call to Invoke is left intact. + // Watch https://github.com/dotnet/roslyn/issues/5835 + return scheduler.Schedule(action, (s, a) => Invoke(s, a)); } /// @@ -64,7 +71,8 @@ public static IDisposable Schedule(this IScheduler scheduler, TimeSpan dueTime, if (action == null) throw new ArgumentNullException(nameof(action)); - return scheduler.Schedule(action, dueTime, Invoke); + // See note above. + return scheduler.Schedule(action, dueTime, (s, a) => Invoke(s, a)); } /// @@ -82,7 +90,8 @@ public static IDisposable Schedule(this IScheduler scheduler, DateTimeOffset due if (action == null) throw new ArgumentNullException(nameof(action)); - return scheduler.Schedule(action, dueTime, Invoke); + // See note above. + return scheduler.Schedule(action, dueTime, (s, a) => Invoke(s, a)); } /// diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/SynchronizationContextScheduler.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/SynchronizationContextScheduler.cs index d65e387f0f..0ec534f1a9 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/SynchronizationContextScheduler.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/SynchronizationContextScheduler.cs @@ -57,22 +57,20 @@ public override IDisposable Schedule(TState state, Func { - _context.PostWithStartComplete(() => + if (!d.IsDisposed) { - if (!d.IsDisposed) - { - d.Disposable = action(this, state); - } - }); - } + d.Disposable = action(this, state); + } + }); return d; } @@ -97,7 +95,7 @@ public override IDisposable Schedule(TState state, TimeSpan dueTime, Fun return Schedule(state, action); } - return DefaultScheduler.Instance.Schedule(state, dt, (_, state1) => Schedule(state1, action)); + return DefaultScheduler.Instance.Schedule((scheduler: this, action, state), dt, (_, tuple) => tuple.scheduler.Schedule(tuple.state, tuple.action)); } } } diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/ThreadPoolScheduler.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/ThreadPoolScheduler.cs index e17d807513..c6ebf915e4 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/ThreadPoolScheduler.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/ThreadPoolScheduler.cs @@ -15,6 +15,35 @@ namespace System.Reactive.Concurrency /// Singleton instance of this type exposed through this static property. public sealed class ThreadPoolScheduler : LocalScheduler, ISchedulerLongRunning, ISchedulerPeriodic { + private sealed class UserWorkItem : IDisposable + { + private IDisposable _cancelRunDisposable; + + private readonly TState _state; + private readonly IScheduler _scheduler; + private readonly Func _action; + + public UserWorkItem(IScheduler scheduler, TState state, Func action) + { + _state = state; + _action = action; + _scheduler = scheduler; + } + + public void Run() + { + if (!Disposable.GetIsDisposed(ref _cancelRunDisposable)) + { + Disposable.TrySetSingle(ref _cancelRunDisposable, _action(_scheduler, _state)); + } + } + + public void Dispose() + { + Disposable.TryDispose(ref _cancelRunDisposable); + } + } + private static readonly Lazy s_instance = new Lazy(() => new ThreadPoolScheduler()); private static readonly Lazy s_newBackgroundThread = new Lazy(() => new NewThreadScheduler(action => new Thread(action) { IsBackground = true })); @@ -40,17 +69,13 @@ public override IDisposable Schedule(TState state, Func(this, state, action); - ThreadPool.QueueUserWorkItem(_ => - { - if (!d.IsDisposed) - { - d.Disposable = action(this, state); - } - }, null); + ThreadPool.QueueUserWorkItem( + closureWorkItem => ((UserWorkItem)closureWorkItem).Run(), + workItem); - return d; + return workItem; } /// @@ -144,15 +169,17 @@ public FastPeriodicTimer(TState state, Func action) _state = state; _action = action; - ThreadPool.QueueUserWorkItem(Tick, null); + ThreadPool.QueueUserWorkItem(_ => Tick(_), this); // Replace with method group as soon as Roslyn will cache the delegate then. } - private void Tick(object state) + private static void Tick(object state) { - if (!_disposed) + var timer = (FastPeriodicTimer)state; + + if (!timer._disposed) { - _state = _action(_state); - ThreadPool.QueueUserWorkItem(Tick, null); + timer._state = timer._action(timer._state); + ThreadPool.QueueUserWorkItem(_ => Tick(_), timer); } } @@ -192,23 +219,25 @@ public Timer(IScheduler parent, TState state, TimeSpan dueTime, Func Tick(_) /* Don't convert to method group until Roslyn catches up */, this, dueTime, TimeSpan.FromMilliseconds(System.Threading.Timeout.Infinite)); } } - private void Tick(object state) + private static void Tick(object state) { + var timer = (Timer)state; + try { - _disposable.Disposable = _action(_parent, _state); + timer._disposable.Disposable = timer._action(timer._parent, timer._state); } finally { - SpinWait.SpinUntil(IsTimerAssigned); - Stop(); + SpinWait.SpinUntil(timer.IsTimerAssigned); + timer.Stop(); } } diff --git a/Rx.NET/Source/src/System.Reactive/Concurrency/VirtualTimeScheduler.Extensions.cs b/Rx.NET/Source/src/System.Reactive/Concurrency/VirtualTimeScheduler.Extensions.cs index f51d766567..c62390f178 100644 --- a/Rx.NET/Source/src/System.Reactive/Concurrency/VirtualTimeScheduler.Extensions.cs +++ b/Rx.NET/Source/src/System.Reactive/Concurrency/VirtualTimeScheduler.Extensions.cs @@ -29,7 +29,11 @@ public static class VirtualTimeSchedulerExtensions if (action == null) throw new ArgumentNullException(nameof(action)); - return scheduler.ScheduleRelative(action, dueTime, Invoke); + // As stated in Scheduler.Simple.cs, + // an anonymous delegate will allow delegate caching. + // Watch https://github.com/dotnet/roslyn/issues/5835 for compiler + // support for caching delegates from method groups. + return scheduler.ScheduleRelative(action, dueTime, (s, a) => Invoke(s, a)); } /// @@ -50,7 +54,7 @@ public static class VirtualTimeSchedulerExtensions if (action == null) throw new ArgumentNullException(nameof(action)); - return scheduler.ScheduleAbsolute(action, dueTime, Invoke); + return scheduler.ScheduleAbsolute(action, dueTime, (s, a) => Invoke(s, a)); } private static IDisposable Invoke(IScheduler scheduler, Action action)