diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index 699186868a..bb1663eac0 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -248,11 +248,11 @@ public partial class Dispatcher /// An operation representing the queued delegate to be invoked. /// /// - /// Note that the default priority is DispatcherPriority.Normal. + /// Note that the default priority is DispatcherPriority.Default. /// public DispatcherOperation InvokeAsync(Action callback) { - return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + return InvokeAsync(callback, default, CancellationToken.None); } /// @@ -326,11 +326,11 @@ public partial class Dispatcher /// An operation representing the queued delegate to be invoked. /// /// - /// Note that the default priority is DispatcherPriority.Normal. + /// Note that the default priority is DispatcherPriority.Default. /// public DispatcherOperation InvokeAsync(Func callback) { - return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + return InvokeAsync(callback, DispatcherPriority.Default, CancellationToken.None); } /// @@ -541,6 +541,18 @@ public partial class Dispatcher InvokeAsyncImpl(new DispatcherOperation(this, priority, action, true), CancellationToken.None); } + /// + /// Executes the specified Func<Task> asynchronously on the + /// thread that the Dispatcher was created on + /// + /// + /// A Func<Task> delegate to invoke through the dispatcher. + /// + /// + /// An task that completes after the task returned from callback finishes. + /// + public Task InvokeAsync(Func callback) => InvokeAsync(callback, DispatcherPriority.Default); + /// /// Executes the specified Func<Task> asynchronously on the /// thread that the Dispatcher was created on @@ -556,11 +568,29 @@ public partial class Dispatcher /// /// An task that completes after the task returned from callback finishes /// - public Task InvokeAsync(Func callback, DispatcherPriority priority = default) + public Task InvokeAsync(Func callback, DispatcherPriority priority) { _ = callback ?? throw new ArgumentNullException(nameof(callback)); return InvokeAsync(callback, priority).GetTask().Unwrap(); } + + /// + /// Executes the specified Func<Task<TResult>> asynchronously on the + /// thread that the Dispatcher was created on + /// + /// + /// A Func<Task<TResult>> delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// An task that completes after the task returned from callback finishes + /// + public Task InvokeAsync(Func> action) => + InvokeAsync(action, DispatcherPriority.Default); /// /// Executes the specified Func<Task<TResult>> asynchronously on the @@ -577,7 +607,7 @@ public partial class Dispatcher /// /// An task that completes after the task returned from callback finishes /// - public Task InvokeAsync(Func> action, DispatcherPriority priority = default) + public Task InvokeAsync(Func> action, DispatcherPriority priority) { _ = action ?? throw new ArgumentNullException(nameof(action)); return InvokeAsync>(action, priority).GetTask().Unwrap(); diff --git a/src/Avalonia.Base/Threading/DispatcherFrame.cs b/src/Avalonia.Base/Threading/DispatcherFrame.cs index 1f8974dfa3..e826432475 100644 --- a/src/Avalonia.Base/Threading/DispatcherFrame.cs +++ b/src/Avalonia.Base/Threading/DispatcherFrame.cs @@ -91,31 +91,44 @@ public class DispatcherFrame internal void Run(IControlledDispatcherImpl impl) { - // Since the actual platform run loop is controlled by a Cancellation token, we are restarting - // it if frame still needs to run - while (Continue) - RunCore(impl); - } - - private void RunCore(IControlledDispatcherImpl impl) - { - if (_isRunning) - throw new InvalidOperationException("This frame is already running"); - _isRunning = true; - try - { - _cancellationTokenSource = new CancellationTokenSource(); - // Wake up the dispatcher in case it has pending jobs - Dispatcher.RequestProcessing(); - impl.RunLoop(_cancellationTokenSource.Token); - } - finally + Dispatcher.VerifyAccess(); + + // Since the actual platform run loop is controlled by a Cancellation token, we have an + // outer loop that restarts the platform one in case Continue was set to true after being set to false + while (true) { - _isRunning = false; - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource = null; + // Take the instance lock since `Continue` is changed from one too + lock (Dispatcher.InstanceLock) + { + if (!Continue) + return; + + if (_isRunning) + throw new InvalidOperationException("This frame is already running"); + + _cancellationTokenSource = new CancellationTokenSource(); + _isRunning = true; + } + + try + { + // Wake up the dispatcher in case it has pending jobs + Dispatcher.RequestProcessing(); + impl.RunLoop(_cancellationTokenSource.Token); + } + finally + { + lock (Dispatcher.InstanceLock) + { + _isRunning = false; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + } } } + internal void MaybeExitOnDispatcherRequest() { diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs index 20aa91c83e..fdc098777a 100644 --- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs +++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs @@ -58,6 +58,10 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl public void RunLoop(CancellationToken token) { + CancellationTokenRegistration registration = default; + if (token.CanBeCanceled) + registration = token.Register(() => _wakeup.Set()); + while (!token.IsCancellationRequested) { bool signaled; @@ -105,5 +109,7 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl else _wakeup.WaitOne(); } + + registration.Dispose(); } } \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index 9ba3f3980d..7b401918ce 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; using Avalonia.Threading; using Avalonia.Utilities; using Xunit; @@ -458,4 +460,46 @@ public class DispatcherTests } } + [Fact] + public void DispatcherInvokeAsyncUnwrapsTasks() + { + int asyncMethodStage = 0; + + async Task AsyncMethod() + { + asyncMethodStage = 1; + await Task.Delay(200); + asyncMethodStage = 2; + } + + async Task AsyncMethodWithResult() + { + await Task.Delay(100); + return 1; + } + + async Task Test() + { + await Dispatcher.UIThread.InvokeAsync(AsyncMethod); + Assert.Equal(2, asyncMethodStage); + Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult)); + asyncMethodStage = 0; + + await Dispatcher.UIThread.InvokeAsync(AsyncMethod, DispatcherPriority.Default); + Assert.Equal(2, asyncMethodStage); + Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult, DispatcherPriority.Default)); + + Dispatcher.UIThread.ExitAllFrames(); + } + + using (new DispatcherServices(new ManagedDispatcherImpl(null))) + { + var t = Test(); + var cts = new CancellationTokenSource(); + Task.Delay(3000).ContinueWith(_ => cts.Cancel()); + Dispatcher.UIThread.MainLoop(cts.Token); + Assert.True(t.IsCompletedSuccessfully); + t.GetAwaiter().GetResult(); + } + } } \ No newline at end of file