Browse Source

Fixed Dispatcher.Invoke when called from the UI thread

pull/10792/head
Nikita Tsukanov 3 years ago
parent
commit
b3fe7827a2
  1. 3
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  2. 8
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  3. 1
      src/Avalonia.Base/Threading/Dispatcher.cs
  4. 65
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  5. 4
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  6. 98
      tests/Avalonia.Base.UnitTests/DispatcherTests.cs

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

@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Threading;
@ -482,7 +483,7 @@ public partial class Dispatcher
// invoke.
try
{
operation.GetTask().Wait();
operation.Wait();
Debug.Assert(operation.Status == DispatcherOperationStatus.Completed ||
operation.Status == DispatcherOperationStatus.Aborted);

8
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Threading;
namespace Avalonia.Threading;
@ -42,11 +43,16 @@ public partial class Dispatcher
/// Force-runs all dispatcher operations ignoring any pending OS events, use with caution
/// </summary>
public void RunJobs(DispatcherPriority? priority = null)
{
RunJobs(priority, CancellationToken.None);
}
internal void RunJobs(DispatcherPriority? priority, CancellationToken cancellationToken)
{
priority ??= DispatcherPriority.MinimumActiveValue;
if (priority < DispatcherPriority.MinimumActiveValue)
priority = DispatcherPriority.MinimumActiveValue;
while (true)
while (!cancellationToken.IsCancellationRequested)
{
DispatcherOperation? job;
lock (InstanceLock)

1
src/Avalonia.Base/Threading/Dispatcher.cs

@ -37,6 +37,7 @@ public partial class Dispatcher : IDispatcher
}
public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher();
public bool SupportsRunLoops => _controlledImpl != null;
private static Dispatcher CreateUIThreadDispatcher()
{

65
src/Avalonia.Base/Threading/DispatcherOperation.cs

@ -111,11 +111,68 @@ public class DispatcherOperation
}
}
public void Wait()
/// <summary>
/// Waits for this operation to complete.
/// </summary>
/// <returns>
/// The status of the operation. To obtain the return value
/// of the invoked delegate, use the the Result property.
/// </returns>
public void Wait() => Wait(TimeSpan.FromMilliseconds(-1));
/// <summary>
/// Waits for this operation to complete.
/// </summary>
/// <param name="timeout">
/// The maximum amount of time to wait.
/// </param>
public void Wait(TimeSpan timeout)
{
if (Dispatcher.CheckAccess())
throw new InvalidOperationException("Wait is only supported on background thread");
GetTask().Wait();
if ((Status == DispatcherOperationStatus.Pending || Status == DispatcherOperationStatus.Executing) &&
timeout.TotalMilliseconds != 0)
{
if (Dispatcher.CheckAccess())
{
if (Status == DispatcherOperationStatus.Executing)
{
// We are the dispatching thread, and the current operation state is
// executing, which means that the operation is in the middle of
// executing (on this thread) and is trying to wait for the execution
// to complete. Unfortunately, the thread will now deadlock, so
// we throw an exception instead.
throw new InvalidOperationException("A thread cannot wait on operations already running on the same thread.");
}
var cts = new CancellationTokenSource();
EventHandler finishedHandler = delegate
{
cts.Cancel();
};
Completed += finishedHandler;
Aborted += finishedHandler;
try
{
while (Status == DispatcherOperationStatus.Pending)
{
if (Dispatcher.SupportsRunLoops)
{
if (Priority >= DispatcherPriority.MinimumForegroundPriority)
Dispatcher.RunJobs(Priority, cts.Token);
else
Dispatcher.MainLoop(cts.Token);
}
else
Dispatcher.RunJobs(DispatcherPriority.MinimumActiveValue, cts.Token);
}
}
finally
{
Completed -= finishedHandler;
Aborted -= finishedHandler;
}
}
}
GetTask().GetAwaiter().GetResult();
}
public Task GetTask() => GetTaskCore();

4
src/Avalonia.Base/Threading/DispatcherPriority.cs

@ -20,7 +20,9 @@ namespace Avalonia.Threading
/// <summary>
/// The lowest foreground dispatcher priority
/// </summary>
internal static readonly DispatcherPriority Default = new(0);
public static readonly DispatcherPriority Default = new(0);
internal static readonly DispatcherPriority MinimumForegroundPriority = Default;
/// <summary>
/// The job will be processed with the same priority as input.

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

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Avalonia.Threading;
using Xunit;
namespace Avalonia.Base.UnitTests;
@ -9,9 +11,14 @@ public class DispatcherTests
{
class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherImplWithPendingInput
{
public bool CurrentThreadIsLoopThread => true;
public void Signal() => AskedForSignal = true;
private Thread _loopThread = Thread.CurrentThread;
private object _lock = new();
public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _loopThread;
public void Signal()
{
lock (_lock)
AskedForSignal = true;
}
public event Action Signaled;
public event Action Timer;
@ -27,9 +34,12 @@ public class DispatcherTests
public void ExecuteSignal()
{
if (!AskedForSignal)
return;
AskedForSignal = false;
lock (_lock)
{
if (!AskedForSignal)
return;
AskedForSignal = false;
}
Signaled?.Invoke();
}
@ -45,6 +55,61 @@ public class DispatcherTests
public bool HasPendingInput => TestInputPending == true;
public bool? TestInputPending { get; set; }
}
class SimpleDispatcherWithBackgroundProcessingImpl : SimpleDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing
{
public bool AskedForBackgroundProcessing { get; private set; }
public event Action ReadyForBackgroundProcessing;
public void RequestBackgroundProcessing()
{
if (!CurrentThreadIsLoopThread)
throw new InvalidOperationException();
AskedForBackgroundProcessing = true;
}
public void FireBackgroundProcessing()
{
if(!AskedForBackgroundProcessing)
return;
AskedForBackgroundProcessing = false;
ReadyForBackgroundProcessing?.Invoke();
}
}
class SimpleControlledDispatcherImpl : SimpleDispatcherWithBackgroundProcessingImpl, IControlledDispatcherImpl
{
private readonly bool _useTestTimeout = true;
private readonly CancellationToken? _cancel;
public int RunLoopCount { get; private set; }
public SimpleControlledDispatcherImpl()
{
}
public SimpleControlledDispatcherImpl(CancellationToken cancel, bool useTestTimeout = false)
{
_useTestTimeout = useTestTimeout;
_cancel = cancel;
}
public void RunLoop(CancellationToken token)
{
RunLoopCount++;
var st = Stopwatch.StartNew();
while (!token.IsCancellationRequested || _cancel?.IsCancellationRequested == true)
{
FireBackgroundProcessing();
ExecuteSignal();
if (_useTestTimeout)
Assert.True(st.ElapsedMilliseconds < 4000, "RunLoop exceeded test time quota");
else
Thread.Sleep(10);
}
}
}
[Fact]
@ -169,5 +234,24 @@ public class DispatcherTests
impl.TestInputPending = false;
}
}
[Theory,
InlineData(false, false),
InlineData(false, true),
InlineData(true, false),
InlineData(true, true)]
public void CanWaitForDispatcherOperationFromTheSameThread(bool controlled, bool foreground)
{
var impl = controlled ? new SimpleControlledDispatcherImpl() : new SimpleDispatcherImpl();
var disp = new Dispatcher(impl);
bool finished = false;
disp.InvokeAsync(() => finished = true,
foreground ? DispatcherPriority.Default : DispatcherPriority.Background).Wait();
Assert.True(finished);
if (controlled)
Assert.Equal(foreground ? 0 : 1, ((SimpleControlledDispatcherImpl)impl).RunLoopCount);
}
}
Loading…
Cancel
Save