소스 검색

Fixed Dispatcher.Invoke when called from the UI thread

Nikita Tsukanov 2 년 전
부모
커밋
b3fe7827a2

+ 2 - 1
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);

+ 7 - 1
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 - 0
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()
     {

+ 61 - 4
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();

+ 3 - 1
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.

+ 91 - 7
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);
+    }
+
 }