committed by
GitHub
27 changed files with 1053 additions and 117 deletions
@ -0,0 +1,216 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Threading; |
||||
|
|
||||
|
public partial class Dispatcher |
||||
|
{ |
||||
|
internal bool ExitAllFramesRequested { get; private set; } |
||||
|
internal bool HasShutdownStarted { get; private set; } |
||||
|
internal int DisabledProcessingCount { get; set; } |
||||
|
private bool _hasShutdownFinished; |
||||
|
private bool _startingShutdown; |
||||
|
|
||||
|
private Stack<DispatcherFrame> _frames = new(); |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raised when the dispatcher is shutting down.
|
||||
|
/// </summary>
|
||||
|
public event EventHandler? ShutdownStarted; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raised when the dispatcher is shut down.
|
||||
|
/// </summary>
|
||||
|
public event EventHandler? ShutdownFinished; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Push an execution frame.
|
||||
|
/// </summary>
|
||||
|
/// <param name="frame">
|
||||
|
/// The frame for the dispatcher to process.
|
||||
|
/// </param>
|
||||
|
public void PushFrame(DispatcherFrame frame) |
||||
|
{ |
||||
|
VerifyAccess(); |
||||
|
if (_controlledImpl == null) |
||||
|
throw new PlatformNotSupportedException(); |
||||
|
_ = frame ?? throw new ArgumentNullException(nameof(frame)); |
||||
|
|
||||
|
if(_hasShutdownFinished) // Dispatcher thread - no lock needed for read
|
||||
|
throw new InvalidOperationException("Cannot perform requested operation because the Dispatcher shut down"); |
||||
|
|
||||
|
if (DisabledProcessingCount > 0) |
||||
|
throw new InvalidOperationException( |
||||
|
"Cannot perform this operation while dispatcher processing is suspended."); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
_frames.Push(frame); |
||||
|
using (AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Normal)) |
||||
|
frame.Run(_controlledImpl); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_frames.Pop(); |
||||
|
if (_frames.Count == 0) |
||||
|
{ |
||||
|
if (HasShutdownStarted) |
||||
|
ShutdownImpl(); |
||||
|
else |
||||
|
ExitAllFramesRequested = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Runs the dispatcher's main loop.
|
||||
|
/// </summary>
|
||||
|
/// <param name="cancellationToken">
|
||||
|
/// A cancellation token used to exit the main loop.
|
||||
|
/// </param>
|
||||
|
public void MainLoop(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (_controlledImpl == null) |
||||
|
throw new PlatformNotSupportedException(); |
||||
|
var frame = new DispatcherFrame(); |
||||
|
cancellationToken.Register(() => frame.Continue = false); |
||||
|
PushFrame(frame); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Requests that all nested frames exit.
|
||||
|
/// </summary>
|
||||
|
public void ExitAllFrames() |
||||
|
{ |
||||
|
if (_frames.Count == 0) |
||||
|
return; |
||||
|
ExitAllFramesRequested = true; |
||||
|
foreach (var f in _frames) |
||||
|
f.MaybeExitOnDispatcherRequest(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Begins the process of shutting down the dispatcher.
|
||||
|
/// </summary>
|
||||
|
public void BeginInvokeShutdown(DispatcherPriority priority) => Post(StartShutdownImpl, priority); |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initiates the shutdown process of the Dispatcher synchronously.
|
||||
|
/// </summary>
|
||||
|
public void InvokeShutdown() => Invoke(StartShutdownImpl, DispatcherPriority.Send); |
||||
|
|
||||
|
private void StartShutdownImpl() |
||||
|
{ |
||||
|
if (!_startingShutdown) |
||||
|
{ |
||||
|
// We only need this to prevent reentrancy if the ShutdownStarted event
|
||||
|
// tries to shut down again.
|
||||
|
_startingShutdown = true; |
||||
|
|
||||
|
// Call the ShutdownStarted event before we actually mark ourselves
|
||||
|
// as shutting down. This is so the handlers can actually do work
|
||||
|
// when they get this event without throwing exceptions.
|
||||
|
ShutdownStarted?.Invoke(this, EventArgs.Empty); |
||||
|
|
||||
|
HasShutdownStarted = true; |
||||
|
|
||||
|
if (_frames.Count > 0) |
||||
|
ExitAllFrames(); |
||||
|
else ShutdownImpl(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private void ShutdownImpl() |
||||
|
{ |
||||
|
DispatcherOperation? operation = null; |
||||
|
_impl.Timer -= PromoteTimers; |
||||
|
_impl.Signaled -= Signaled; |
||||
|
do |
||||
|
{ |
||||
|
lock (InstanceLock) |
||||
|
{ |
||||
|
if (_queue.MaxPriority != DispatcherPriority.Invalid) |
||||
|
{ |
||||
|
operation = _queue.Peek(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
operation = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (operation != null) |
||||
|
{ |
||||
|
operation.Abort(); |
||||
|
} |
||||
|
} while (operation != null); |
||||
|
|
||||
|
_impl.UpdateTimer(null); |
||||
|
_hasShutdownFinished = true; |
||||
|
ShutdownFinished?.Invoke(this, EventArgs.Empty); |
||||
|
} |
||||
|
|
||||
|
public record struct DispatcherProcessingDisabled : IDisposable |
||||
|
{ |
||||
|
private readonly SynchronizationContext? _oldContext; |
||||
|
|
||||
|
private readonly bool _restoreContext; |
||||
|
private Dispatcher? _dispatcher; |
||||
|
|
||||
|
internal DispatcherProcessingDisabled(Dispatcher dispatcher) |
||||
|
{ |
||||
|
_dispatcher = dispatcher; |
||||
|
} |
||||
|
|
||||
|
internal DispatcherProcessingDisabled(Dispatcher dispatcher, SynchronizationContext? oldContext) : this( |
||||
|
dispatcher) |
||||
|
{ |
||||
|
_oldContext = oldContext; |
||||
|
_restoreContext = true; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if(_dispatcher==null) |
||||
|
return; |
||||
|
_dispatcher.DisabledProcessingCount--; |
||||
|
_dispatcher = null; |
||||
|
if (_restoreContext) |
||||
|
SynchronizationContext.SetSynchronizationContext(_oldContext); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disable the event processing of the dispatcher.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This is an advanced method intended to eliminate the chance of
|
||||
|
/// unrelated reentrancy. The effect of disabling processing is:
|
||||
|
/// 1) CLR locks will not pump messages internally.
|
||||
|
/// 2) No one is allowed to push a frame.
|
||||
|
/// 3) No message processing is permitted.
|
||||
|
/// </remarks>
|
||||
|
public DispatcherProcessingDisabled DisableProcessing() |
||||
|
{ |
||||
|
VerifyAccess(); |
||||
|
|
||||
|
// Turn off processing.
|
||||
|
DisabledProcessingCount++; |
||||
|
var oldContext = SynchronizationContext.Current; |
||||
|
if (oldContext is AvaloniaSynchronizationContext or NonPumpingSyncContext) |
||||
|
return new DispatcherProcessingDisabled(this); |
||||
|
|
||||
|
var helper = AvaloniaLocator.Current.GetService<NonPumpingLockHelper.IHelperImpl>(); |
||||
|
if (helper == null) |
||||
|
return new DispatcherProcessingDisabled(this); |
||||
|
|
||||
|
SynchronizationContext.SetSynchronizationContext(new NonPumpingSyncContext(helper, oldContext)); |
||||
|
return new DispatcherProcessingDisabled(this, oldContext); |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
using System; |
||||
|
using System.Threading; |
||||
|
|
||||
|
namespace Avalonia.Threading; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Representation of Dispatcher frame.
|
||||
|
/// </summary>
|
||||
|
public class DispatcherFrame |
||||
|
{ |
||||
|
private bool _exitWhenRequested; |
||||
|
private bool _continue; |
||||
|
private bool _isRunning; |
||||
|
private CancellationTokenSource? _cancellationTokenSource; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Constructs a new instance of the DispatcherFrame class.
|
||||
|
/// </summary>
|
||||
|
public DispatcherFrame() : this(true) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public Dispatcher Dispatcher { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Constructs a new instance of the DispatcherFrame class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="exitWhenRequested">
|
||||
|
/// Indicates whether or not this frame will exit when all frames
|
||||
|
/// are requested to exit.
|
||||
|
/// <p/>
|
||||
|
/// Dispatcher frames typically break down into two categories:
|
||||
|
/// 1) Long running, general purpose frames, that exit only when
|
||||
|
/// told to. These frames should exit when requested.
|
||||
|
/// 2) Short running, very specific frames that exit themselves
|
||||
|
/// when an important criteria is met. These frames may
|
||||
|
/// consider not exiting when requested in favor of waiting
|
||||
|
/// for their important criteria to be met. These frames
|
||||
|
/// should have a timeout associated with them.
|
||||
|
/// </param>
|
||||
|
public DispatcherFrame(bool exitWhenRequested) |
||||
|
{ |
||||
|
Dispatcher = Dispatcher.UIThread; |
||||
|
Dispatcher.VerifyAccess(); |
||||
|
_exitWhenRequested = exitWhenRequested; |
||||
|
_continue = true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates that this dispatcher frame should exit.
|
||||
|
/// </summary>
|
||||
|
public bool Continue |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
// This method is free-threaded.
|
||||
|
|
||||
|
// First check if this frame wants to continue.
|
||||
|
bool shouldContinue = _continue; |
||||
|
if (shouldContinue) |
||||
|
{ |
||||
|
// This frame wants to continue, so next check if it will
|
||||
|
// respect the "exit requests" from the dispatcher.
|
||||
|
if (_exitWhenRequested) |
||||
|
{ |
||||
|
Dispatcher dispatcher = Dispatcher; |
||||
|
|
||||
|
// This frame is willing to respect the "exit requests" of
|
||||
|
// the dispatcher, so check them.
|
||||
|
if (dispatcher.ExitAllFramesRequested || dispatcher.HasShutdownStarted) |
||||
|
{ |
||||
|
shouldContinue = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return shouldContinue; |
||||
|
} |
||||
|
|
||||
|
set |
||||
|
{ |
||||
|
// This method is free-threaded.
|
||||
|
lock (Dispatcher.InstanceLock) |
||||
|
{ |
||||
|
_continue = value; |
||||
|
if (!_continue) |
||||
|
_cancellationTokenSource?.Cancel(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
{ |
||||
|
_isRunning = false; |
||||
|
_cancellationTokenSource?.Cancel(); |
||||
|
_cancellationTokenSource = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal void MaybeExitOnDispatcherRequest() |
||||
|
{ |
||||
|
if (_exitWhenRequested) |
||||
|
_cancellationTokenSource?.Cancel(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Utilities; |
||||
|
using Avalonia.Win32.Interop; |
||||
|
|
||||
|
namespace Avalonia.Win32; |
||||
|
|
||||
|
internal class NonPumpingWaitHelperImpl : NonPumpingLockHelper.IHelperImpl |
||||
|
{ |
||||
|
public static NonPumpingWaitHelperImpl Instance { get; } = new(); |
||||
|
public int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) => |
||||
|
UnmanagedMethods.WaitForMultipleObjectsEx(waitHandles.Length, waitHandles, waitAll, |
||||
|
millisecondsTimeout, false); |
||||
|
} |
||||
Loading…
Reference in new issue