Browse Source

Implemented Dispatcher.Yield / Dispatcher.Resume (#19370)

* Refactored DispatcherPriorityAwaitable, implemented Dispatcher.Yield

* Picked changes from https://github.com/AvaloniaUI/Avalonia/pull/14212

* Format/compile

* Update API suppressions

---------

Co-authored-by: Yoh Deadfall <yoh.deadfall@hotmail.com>
Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/19387/head
Nikita Tsukanov 6 months ago
committed by GitHub
parent
commit
f3b418d435
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 60
      api/Avalonia.nupkg.xml
  2. 66
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  3. 122
      src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs
  4. 100
      tests/Avalonia.Base.UnitTests/DispatcherTests.cs

60
api/Avalonia.nupkg.xml

@ -109,6 +109,42 @@
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.get_IsCompleted</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.GetAwaiter</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.GetResult</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.OnCompleted(System.Action)</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetAwaiter</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetResult</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect})</Target>
@ -187,6 +223,30 @@
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable`1</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable`1</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0009</DiagnosticId>
<Target>T:Avalonia.Diagnostics.StyleDiagnostics</Target>

66
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@ -672,4 +672,70 @@ public partial class Dispatcher
/// </summary>
public DispatcherPriorityAwaitable<T> AwaitWithPriority<T>(Task<T> task, DispatcherPriority priority) =>
new(this, task, priority);
/// <summary>
/// Creates an awaitable object that asynchronously resumes execution on the dispatcher.
/// </summary>
/// <returns>
/// An awaitable object that asynchronously resumes execution on the dispatcher.
/// </returns>
/// <remarks>
/// This method is equivalent to calling the <see cref="Resume(DispatcherPriority)"/> method
/// and passing in <see cref="DispatcherPriority.Background"/>.
/// </remarks>
public DispatcherPriorityAwaitable Resume() =>
Resume(DispatcherPriority.Background);
/// <summary>``
/// Creates an awaitable object that asynchronously resumes execution on the dispatcher. The work that occurs
/// when control returns to the code awaiting the result of this method is scheduled with the specified priority.
/// </summary>
/// <param name="priority">The priority at which to schedule the continuation.</param>
/// <returns>
/// An awaitable object that asynchronously resumes execution on the dispatcher.
/// </returns>
public DispatcherPriorityAwaitable Resume(DispatcherPriority priority)
{
DispatcherPriority.Validate(priority, nameof(priority));
return new(this, null, priority);
}
/// <summary>
/// Creates an awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </summary>
/// <returns>
/// An awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </returns>
/// <remarks>
/// This method is equivalent to calling the <see cref="Yield(DispatcherPriority)"/> method
/// and passing in <see cref="DispatcherPriority.Background"/>.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The current thread is not the UI thread.
/// </exception>
public static DispatcherPriorityAwaitable Yield() =>
Yield(DispatcherPriority.Background);
/// <summary>
/// Creates an cawaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events. The work that occurs when
/// control returns to the code awaiting the result of this method is scheduled with the specified priority.
/// </summary>
/// <param name="priority">The priority at which to schedule the continuation.</param>
/// <returns>
/// An awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The current thread is not the UI thread.
/// </exception>
public static DispatcherPriorityAwaitable Yield(DispatcherPriority priority)
{
// TODO12: Update to use Dispatcher.CurrentDispatcher once multi-dispatcher support is merged
var current = UIThread;
current.VerifyAccess();
return UIThread.Resume(priority);
}
}

122
src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs

@ -1,40 +1,130 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Threading;
public class DispatcherPriorityAwaitable : INotifyCompletion
/// <summary>
/// A simple awaitable type that will return a DispatcherPriorityAwaiter.
/// </summary>
public struct DispatcherPriorityAwaitable
{
private readonly Dispatcher _dispatcher;
private protected readonly Task Task;
private readonly Task? _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task task, DispatcherPriority priority)
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task? task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
Task = task;
_task = task;
_priority = priority;
}
public void OnCompleted(Action continuation) =>
Task.ContinueWith(_ => _dispatcher.Post(continuation, _priority));
public bool IsCompleted => Task.IsCompleted;
public DispatcherPriorityAwaiter GetAwaiter() => new(_dispatcher, _task, _priority);
}
/// <summary>
/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority.
/// </summary>
/// <remarks>
/// This is returned from DispatcherPriorityAwaitable.GetAwaiter()
/// </remarks>
public struct DispatcherPriorityAwaiter : INotifyCompletion
{
private readonly Dispatcher _dispatcher;
private readonly Task? _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task? task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public void OnCompleted(Action continuation)
{
if(_task == null || _task.IsCompleted)
_dispatcher.Post(continuation, _priority);
else
{
var self = this;
_task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
{
self._dispatcher.Post(continuation, self._priority);
});
}
}
/// <summary>
/// This always returns false since continuation is requested to be queued to a dispatcher queue
/// </summary>
public bool IsCompleted => false;
public void GetResult()
{
if (_task != null)
_task.GetAwaiter().GetResult();
}
}
/// <summary>
/// A simple awaitable type that will return a DispatcherPriorityAwaiter&lt;T&gt;.
/// </summary>
public struct DispatcherPriorityAwaitable<T>
{
private readonly Dispatcher _dispatcher;
private readonly Task<T> _task;
private readonly DispatcherPriority _priority;
public void GetResult() => Task.GetAwaiter().GetResult();
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public DispatcherPriorityAwaitable GetAwaiter() => this;
public DispatcherPriorityAwaiter<T> GetAwaiter() => new(_dispatcher, _task, _priority);
}
public sealed class DispatcherPriorityAwaitable<T> : DispatcherPriorityAwaitable
/// <summary>
/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority.
/// </summary>
/// <remarks>
/// This is returned from DispatcherPriorityAwaitable&lt;T&gt;.GetAwaiter()
/// </remarks>
public struct DispatcherPriorityAwaiter<T> : INotifyCompletion
{
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority) : base(
dispatcher, task, priority)
private readonly Dispatcher _dispatcher;
private readonly Task<T> _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public new T GetResult() => ((Task<T>)Task).GetAwaiter().GetResult();
public void OnCompleted(Action continuation)
{
if(_task.IsCompleted)
_dispatcher.Post(continuation, _priority);
else
{
var self = this;
_task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
{
self._dispatcher.Post(continuation, self._priority);
});
}
}
/// <summary>
/// This always returns false since continuation is requested to be queued to a dispatcher queue
/// </summary>
public bool IsCompleted => false;
public new DispatcherPriorityAwaitable<T> GetAwaiter() => this;
}
public void GetResult() => _task.GetAwaiter().GetResult();
}

100
tests/Avalonia.Base.UnitTests/DispatcherTests.cs

@ -505,4 +505,104 @@ public partial class DispatcherTests
t.GetAwaiter().GetResult();
}
}
[Fact]
public async Task DispatcherResumeContinuesOnUIThread()
{
using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
var tokenSource = new CancellationTokenSource();
var workload = Dispatcher.UIThread.InvokeAsync(
async () =>
{
Assert.True(Dispatcher.UIThread.CheckAccess());
await Task.Delay(1).ConfigureAwait(false);
Assert.False(Dispatcher.UIThread.CheckAccess());
await Dispatcher.UIThread.Resume();
Assert.True(Dispatcher.UIThread.CheckAccess());
tokenSource.Cancel();
});
Dispatcher.UIThread.MainLoop(tokenSource.Token);
}
[Fact]
public async Task DispatcherYieldContinuesOnUIThread()
{
using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
var tokenSource = new CancellationTokenSource();
var workload = Dispatcher.UIThread.InvokeAsync(
async () =>
{
Assert.True(Dispatcher.UIThread.CheckAccess());
await Dispatcher.Yield();
Assert.True(Dispatcher.UIThread.CheckAccess());
tokenSource.Cancel();
});
Dispatcher.UIThread.MainLoop(tokenSource.Token);
}
[Fact]
public async Task DispatcherYieldThrowsOnNonUIThread()
{
using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
var tokenSource = new CancellationTokenSource();
var workload = Dispatcher.UIThread.InvokeAsync(
async () =>
{
Assert.True(Dispatcher.UIThread.CheckAccess());
await Task.Delay(1).ConfigureAwait(false);
Assert.False(Dispatcher.UIThread.CheckAccess());
await Assert.ThrowsAsync<InvalidOperationException>(async () => await Dispatcher.Yield());
tokenSource.Cancel();
});
Dispatcher.UIThread.MainLoop(tokenSource.Token);
}
[Fact]
public async Task AwaitWithPriorityRunsOnUIThread()
{
static async Task<int> Workload()
{
await Task.Delay(1).ConfigureAwait(false);
Assert.False(Dispatcher.UIThread.CheckAccess());
return Thread.CurrentThread.ManagedThreadId;
}
using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
var tokenSource = new CancellationTokenSource();
var workload = Dispatcher.UIThread.InvokeAsync(
async () =>
{
Assert.True(Dispatcher.UIThread.CheckAccess());
Task taskWithoutResult = Workload();
await Dispatcher.UIThread.AwaitWithPriority(taskWithoutResult, DispatcherPriority.Default);
Assert.True(Dispatcher.UIThread.CheckAccess());
Task<int> taskWithResult = Workload();
await Dispatcher.UIThread.AwaitWithPriority(taskWithResult, DispatcherPriority.Default);
Assert.True(Dispatcher.UIThread.CheckAccess());
tokenSource.Cancel();
});
Dispatcher.UIThread.MainLoop(tokenSource.Token);
}
}

Loading…
Cancel
Save