committed by
GitHub
127 changed files with 4773 additions and 1947 deletions
@ -0,0 +1,553 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Diagnostics; |
|||
using System.Threading; |
|||
|
|||
namespace Avalonia.Threading; |
|||
|
|||
public partial class Dispatcher |
|||
{ |
|||
/// <summary>
|
|||
/// Executes the specified Action synchronously on the thread that
|
|||
/// the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// An Action delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <remarks>
|
|||
/// Note that the default priority is DispatcherPriority.Send.
|
|||
/// </remarks>
|
|||
public void Invoke(Action callback) |
|||
{ |
|||
Invoke(callback, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Action synchronously on the thread that
|
|||
/// the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// An Action delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
public void Invoke(Action callback, DispatcherPriority priority) |
|||
{ |
|||
Invoke(callback, priority, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Action synchronously on the thread that
|
|||
/// the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// An Action delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <param name="cancellationToken">
|
|||
/// A cancellation token that can be used to cancel the operation.
|
|||
/// If the operation has not started, it will be aborted when the
|
|||
/// cancellation token is canceled. If the operation has started,
|
|||
/// the operation can cooperate with the cancellation request.
|
|||
/// </param>
|
|||
public void Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken) |
|||
{ |
|||
Invoke(callback, priority, cancellationToken, TimeSpan.FromMilliseconds(-1)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Action synchronously on the thread that
|
|||
/// the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// An Action delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <param name="cancellationToken">
|
|||
/// A cancellation token that can be used to cancel the operation.
|
|||
/// If the operation has not started, it will be aborted when the
|
|||
/// cancellation token is canceled. If the operation has started,
|
|||
/// the operation can cooperate with the cancellation request.
|
|||
/// </param>
|
|||
/// <param name="timeout">
|
|||
/// The minimum amount of time to wait for the operation to start.
|
|||
/// Once the operation has started, it will complete before this method
|
|||
/// returns.
|
|||
/// </param>
|
|||
public void Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken, |
|||
TimeSpan timeout) |
|||
{ |
|||
if (callback == null) |
|||
{ |
|||
throw new ArgumentNullException("callback"); |
|||
} |
|||
|
|||
DispatcherPriority.Validate(priority, "priority"); |
|||
|
|||
if (timeout.TotalMilliseconds < 0 && |
|||
timeout != TimeSpan.FromMilliseconds(-1)) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("timeout"); |
|||
} |
|||
|
|||
// Fast-Path: if on the same thread, and invoking at Send priority,
|
|||
// and the cancellation token is not already canceled, then just
|
|||
// call the callback directly.
|
|||
if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) |
|||
{ |
|||
callback(); |
|||
return; |
|||
} |
|||
|
|||
// Slow-Path: go through the queue.
|
|||
DispatcherOperation operation = new DispatcherOperation(this, priority, callback, false); |
|||
InvokeImpl(operation, cancellationToken, timeout); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Func<TResult> synchronously on the
|
|||
/// thread that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// A Func<TResult> delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// The return value from the delegate being invoked.
|
|||
/// </returns>
|
|||
/// <remarks>
|
|||
/// Note that the default priority is DispatcherPriority.Send.
|
|||
/// </remarks>
|
|||
public TResult Invoke<TResult>(Func<TResult> callback) |
|||
{ |
|||
return Invoke(callback, DispatcherPriority.Send, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Func<TResult> synchronously on the
|
|||
/// thread that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// A Func<TResult> delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// The return value from the delegate being invoked.
|
|||
/// </returns>
|
|||
public TResult Invoke<TResult>(Func<TResult> callback, DispatcherPriority priority) |
|||
{ |
|||
return Invoke(callback, priority, CancellationToken.None, TimeSpan.FromMilliseconds(-1)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Func<TResult> synchronously on the
|
|||
/// thread that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// A Func<TResult> delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <param name="cancellationToken">
|
|||
/// A cancellation token that can be used to cancel the operation.
|
|||
/// If the operation has not started, it will be aborted when the
|
|||
/// cancellation token is canceled. If the operation has started,
|
|||
/// the operation can cooperate with the cancellation request.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// The return value from the delegate being invoked.
|
|||
/// </returns>
|
|||
public TResult Invoke<TResult>(Func<TResult> callback, DispatcherPriority priority, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
return Invoke(callback, priority, cancellationToken, TimeSpan.FromMilliseconds(-1)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Func<TResult> synchronously on the
|
|||
/// thread that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// A Func<TResult> delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <param name="cancellationToken">
|
|||
/// A cancellation token that can be used to cancel the operation.
|
|||
/// If the operation has not started, it will be aborted when the
|
|||
/// cancellation token is canceled. If the operation has started,
|
|||
/// the operation can cooperate with the cancellation request.
|
|||
/// </param>
|
|||
/// <param name="timeout">
|
|||
/// The minimum amount of time to wait for the operation to start.
|
|||
/// Once the operation has started, it will complete before this method
|
|||
/// returns.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// The return value from the delegate being invoked.
|
|||
/// </returns>
|
|||
public TResult Invoke<TResult>(Func<TResult> callback, DispatcherPriority priority, |
|||
CancellationToken cancellationToken, TimeSpan timeout) |
|||
{ |
|||
if (callback == null) |
|||
{ |
|||
throw new ArgumentNullException("callback"); |
|||
} |
|||
|
|||
DispatcherPriority.Validate(priority, "priority"); |
|||
|
|||
if (timeout.TotalMilliseconds < 0 && |
|||
timeout != TimeSpan.FromMilliseconds(-1)) |
|||
{ |
|||
throw new ArgumentOutOfRangeException("timeout"); |
|||
} |
|||
|
|||
// Fast-Path: if on the same thread, and invoking at Send priority,
|
|||
// and the cancellation token is not already canceled, then just
|
|||
// call the callback directly.
|
|||
if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess()) |
|||
{ |
|||
return callback(); |
|||
} |
|||
|
|||
// Slow-Path: go through the queue.
|
|||
DispatcherOperation<TResult> operation = new DispatcherOperation<TResult>(this, priority, callback); |
|||
return (TResult)InvokeImpl(operation, cancellationToken, timeout)!; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Action asynchronously on the thread
|
|||
/// that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// An Action delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An operation representing the queued delegate to be invoked.
|
|||
/// </returns>
|
|||
/// <remarks>
|
|||
/// Note that the default priority is DispatcherPriority.Normal.
|
|||
/// </remarks>
|
|||
public DispatcherOperation InvokeAsync(Action callback) |
|||
{ |
|||
return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Action asynchronously on the thread
|
|||
/// that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// An Action delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An operation representing the queued delegate to be invoked.
|
|||
/// </returns>
|
|||
/// <returns>
|
|||
/// An operation representing the queued delegate to be invoked.
|
|||
/// </returns>
|
|||
public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority) |
|||
{ |
|||
return InvokeAsync(callback, priority, CancellationToken.None); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Action asynchronously on the thread
|
|||
/// that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// An Action delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <param name="cancellationToken">
|
|||
/// A cancellation token that can be used to cancel the operation.
|
|||
/// If the operation has not started, it will be aborted when the
|
|||
/// cancellation token is canceled. If the operation has started,
|
|||
/// the operation can cooperate with the cancellation request.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An operation representing the queued delegate to be invoked.
|
|||
/// </returns>
|
|||
public DispatcherOperation InvokeAsync(Action callback, DispatcherPriority priority, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
if (callback == null) |
|||
{ |
|||
throw new ArgumentNullException("callback"); |
|||
} |
|||
|
|||
DispatcherPriority.Validate(priority, "priority"); |
|||
|
|||
DispatcherOperation operation = new DispatcherOperation(this, priority, callback, false); |
|||
InvokeAsyncImpl(operation, cancellationToken); |
|||
|
|||
return operation; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Func<TResult> asynchronously on the
|
|||
/// thread that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// A Func<TResult> delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An operation representing the queued delegate to be invoked.
|
|||
/// </returns>
|
|||
/// <remarks>
|
|||
/// Note that the default priority is DispatcherPriority.Normal.
|
|||
/// </remarks>
|
|||
public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback) |
|||
{ |
|||
return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Func<TResult> asynchronously on the
|
|||
/// thread that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// A Func<TResult> delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An operation representing the queued delegate to be invoked.
|
|||
/// </returns>
|
|||
public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback, DispatcherPriority priority) |
|||
{ |
|||
return InvokeAsync(callback, priority, CancellationToken.None); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Executes the specified Func<TResult> asynchronously on the
|
|||
/// thread that the Dispatcher was created on.
|
|||
/// </summary>
|
|||
/// <param name="callback">
|
|||
/// A Func<TResult> delegate to invoke through the dispatcher.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority that determines in what order the specified
|
|||
/// callback is invoked relative to the other pending operations
|
|||
/// in the Dispatcher.
|
|||
/// </param>
|
|||
/// <param name="cancellationToken">
|
|||
/// A cancellation token that can be used to cancel the operation.
|
|||
/// If the operation has not started, it will be aborted when the
|
|||
/// cancellation token is canceled. If the operation has started,
|
|||
/// the operation can cooperate with the cancellation request.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// An operation representing the queued delegate to be invoked.
|
|||
/// </returns>
|
|||
public DispatcherOperation<TResult> InvokeAsync<TResult>(Func<TResult> callback, DispatcherPriority priority, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
if (callback == null) |
|||
{ |
|||
throw new ArgumentNullException("callback"); |
|||
} |
|||
|
|||
DispatcherPriority.Validate(priority, "priority"); |
|||
|
|||
DispatcherOperation<TResult> operation = new DispatcherOperation<TResult>(this, priority, callback); |
|||
InvokeAsyncImpl(operation, cancellationToken); |
|||
|
|||
return operation; |
|||
} |
|||
|
|||
private void InvokeAsyncImpl(DispatcherOperation operation, CancellationToken cancellationToken) |
|||
{ |
|||
bool succeeded = false; |
|||
|
|||
// Could be a non-dispatcher thread, lock to read
|
|||
lock (InstanceLock) |
|||
{ |
|||
if (!cancellationToken.IsCancellationRequested && |
|||
!_hasShutdownFinished && |
|||
!Environment.HasShutdownStarted) |
|||
{ |
|||
// Add the operation to the work queue
|
|||
_queue.Enqueue(operation.Priority, operation); |
|||
|
|||
// Make sure we will wake up to process this operation.
|
|||
succeeded = RequestProcessing(); |
|||
|
|||
if (!succeeded) |
|||
{ |
|||
// Dequeue the item since we failed to request
|
|||
// processing for it. Note we will mark it aborted
|
|||
// below.
|
|||
_queue.RemoveItem(operation); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (succeeded == true) |
|||
{ |
|||
// We have enqueued the operation. Register a callback
|
|||
// with the cancellation token to abort the operation
|
|||
// when cancellation is requested.
|
|||
if (cancellationToken.CanBeCanceled) |
|||
{ |
|||
CancellationTokenRegistration cancellationRegistration = |
|||
cancellationToken.Register(s => ((DispatcherOperation)s!).Abort(), operation); |
|||
|
|||
// Revoke the cancellation when the operation is done.
|
|||
operation.Aborted += (s, e) => cancellationRegistration.Dispose(); |
|||
operation.Completed += (s, e) => cancellationRegistration.Dispose(); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// We failed to enqueue the operation, and the caller that
|
|||
// created the operation does not expose it before we return,
|
|||
// so it is safe to modify the operation outside of the lock.
|
|||
// Just mark the operation as aborted, which we can safely
|
|||
// return to the user.
|
|||
operation.DoAbort(); |
|||
} |
|||
} |
|||
|
|||
|
|||
private object? InvokeImpl(DispatcherOperation operation, CancellationToken cancellationToken, TimeSpan timeout) |
|||
{ |
|||
object? result = null; |
|||
|
|||
Debug.Assert(timeout.TotalMilliseconds >= 0 || timeout == TimeSpan.FromMilliseconds(-1)); |
|||
Debug.Assert(operation.Priority != DispatcherPriority.Send || !CheckAccess()); // should be handled by caller
|
|||
|
|||
if (!cancellationToken.IsCancellationRequested) |
|||
{ |
|||
// This operation must be queued since it was invoked either to
|
|||
// another thread, or at a priority other than Send.
|
|||
InvokeAsyncImpl(operation, cancellationToken); |
|||
|
|||
CancellationToken ctTimeout = CancellationToken.None; |
|||
CancellationTokenRegistration ctTimeoutRegistration = new CancellationTokenRegistration(); |
|||
CancellationTokenSource? ctsTimeout = null; |
|||
|
|||
if (timeout.TotalMilliseconds >= 0) |
|||
{ |
|||
// Create a CancellationTokenSource that will abort the
|
|||
// operation after the timeout. Note that this does not
|
|||
// cancel the operation, just abort it if it is still pending.
|
|||
ctsTimeout = new CancellationTokenSource(timeout); |
|||
ctTimeout = ctsTimeout.Token; |
|||
ctTimeoutRegistration = ctTimeout.Register(s => ((DispatcherOperation)s!).Abort(), operation); |
|||
} |
|||
|
|||
|
|||
// We have already registered with the cancellation tokens
|
|||
// (both provided by the user, and one for the timeout) to
|
|||
// abort the operation when they are canceled. If the
|
|||
// operation has already started when the timeout expires,
|
|||
// we still wait for it to complete. This is different
|
|||
// than simply waiting on the operation with a timeout
|
|||
// because we are the ones queueing the dispatcher
|
|||
// operation, not the caller. We can't leave the operation
|
|||
// in a state that it might execute if we return that it did not
|
|||
// invoke.
|
|||
try |
|||
{ |
|||
operation.GetTask().Wait(); |
|||
|
|||
Debug.Assert(operation.Status == DispatcherOperationStatus.Completed || |
|||
operation.Status == DispatcherOperationStatus.Aborted); |
|||
|
|||
// Old async semantics return from Wait without
|
|||
// throwing an exception if the operation was aborted.
|
|||
// There is no need to test the timout condition, since
|
|||
// the old async semantics would just return the result,
|
|||
// which would be null.
|
|||
|
|||
// This should not block because either the operation
|
|||
// is using the old async sematics, or the operation
|
|||
// completed successfully.
|
|||
result = operation.GetResult(); |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
Debug.Assert(operation.Status == DispatcherOperationStatus.Aborted); |
|||
|
|||
// New async semantics will throw an exception if the
|
|||
// operation was aborted. Here we convert that
|
|||
// exception into a timeout exception if the timeout
|
|||
// has expired (admittedly a weak relationship
|
|||
// assuming causality).
|
|||
if (ctTimeout.IsCancellationRequested) |
|||
{ |
|||
// The operation was canceled because of the
|
|||
// timeout, throw a TimeoutException instead.
|
|||
throw new TimeoutException(); |
|||
} |
|||
else |
|||
{ |
|||
// The operation was canceled from some other reason.
|
|||
throw; |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
ctTimeoutRegistration.Dispose(); |
|||
if (ctsTimeout != null) |
|||
{ |
|||
ctsTimeout.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Post(Action action, DispatcherPriority priority = default) |
|||
{ |
|||
_ = action ?? throw new ArgumentNullException(nameof(action)); |
|||
InvokeAsyncImpl(new DispatcherOperation(this, priority, action, true), CancellationToken.None); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Posts an action that will be invoked on the dispatcher thread.
|
|||
/// </summary>
|
|||
/// <param name="action">The method.</param>
|
|||
/// <param name="arg">The argument of method to call.</param>
|
|||
/// <param name="priority">The priority with which to invoke the method.</param>
|
|||
public void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default) |
|||
{ |
|||
_ = action ?? throw new ArgumentNullException(nameof(action)); |
|||
InvokeAsyncImpl(new SendOrPostCallbackDispatcherOperation(this, priority, action, arg, true), CancellationToken.None); |
|||
} |
|||
} |
|||
@ -0,0 +1,238 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
|
|||
namespace Avalonia.Threading; |
|||
|
|||
public partial class Dispatcher |
|||
{ |
|||
private readonly DispatcherPriorityQueue _queue = new(); |
|||
private bool _signaled; |
|||
private bool _explicitBackgroundProcessingRequested; |
|||
private const int MaximumTimeProcessingBackgroundJobs = 50; |
|||
|
|||
void RequestBackgroundProcessing() |
|||
{ |
|||
lock (InstanceLock) |
|||
{ |
|||
if (_backgroundProcessingImpl != null) |
|||
{ |
|||
if(_explicitBackgroundProcessingRequested) |
|||
return; |
|||
_explicitBackgroundProcessingRequested = true; |
|||
_backgroundProcessingImpl.RequestBackgroundProcessing(); |
|||
} |
|||
else if (_dueTimeForBackgroundProcessing == null) |
|||
{ |
|||
_dueTimeForBackgroundProcessing = Now + 1; |
|||
UpdateOSTimer(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void OnReadyForExplicitBackgroundProcessing() |
|||
{ |
|||
lock (InstanceLock) |
|||
{ |
|||
_explicitBackgroundProcessingRequested = false; |
|||
ExecuteJobsCore(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Force-runs all dispatcher operations ignoring any pending OS events, use with caution
|
|||
/// </summary>
|
|||
public void RunJobs(DispatcherPriority? priority = null) |
|||
{ |
|||
priority ??= DispatcherPriority.MinimumActiveValue; |
|||
if (priority < DispatcherPriority.MinimumActiveValue) |
|||
priority = DispatcherPriority.MinimumActiveValue; |
|||
while (true) |
|||
{ |
|||
DispatcherOperation? job; |
|||
lock (InstanceLock) |
|||
job = _queue.Peek(); |
|||
if (job == null) |
|||
return; |
|||
if (priority != null && job.Priority < priority.Value) |
|||
return; |
|||
ExecuteJob(job); |
|||
} |
|||
} |
|||
|
|||
class DummyShuttingDownUnitTestDispatcherImpl : IDispatcherImpl |
|||
{ |
|||
public bool CurrentThreadIsLoopThread => true; |
|||
public void Signal() |
|||
{ |
|||
} |
|||
|
|||
public event Action? Signaled; |
|||
public event Action? Timer; |
|||
public long Now => 0; |
|||
public void UpdateTimer(long? dueTimeInMs) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
internal static void ResetForUnitTests() |
|||
{ |
|||
if (s_uiThread == null) |
|||
return; |
|||
var st = Stopwatch.StartNew(); |
|||
while (true) |
|||
{ |
|||
s_uiThread._pendingInputImpl = s_uiThread._controlledImpl = null; |
|||
s_uiThread._impl = new DummyShuttingDownUnitTestDispatcherImpl(); |
|||
if (st.Elapsed.TotalSeconds > 5) |
|||
throw new InvalidProgramException("You've caused dispatcher loop"); |
|||
|
|||
DispatcherOperation? job; |
|||
lock (s_uiThread.InstanceLock) |
|||
job = s_uiThread._queue.Peek(); |
|||
if (job == null || job.Priority <= DispatcherPriority.Inactive) |
|||
{ |
|||
s_uiThread = null; |
|||
return; |
|||
} |
|||
|
|||
s_uiThread.ExecuteJob(job); |
|||
} |
|||
|
|||
} |
|||
|
|||
private void ExecuteJob(DispatcherOperation job) |
|||
{ |
|||
lock (InstanceLock) |
|||
_queue.RemoveItem(job); |
|||
job.Execute(); |
|||
// The backend might be firing timers with a low priority,
|
|||
// so we manually check if our high priority timers are due for execution
|
|||
PromoteTimers(); |
|||
} |
|||
|
|||
private void Signaled() |
|||
{ |
|||
lock (InstanceLock) |
|||
_signaled = false; |
|||
|
|||
ExecuteJobsCore(); |
|||
} |
|||
|
|||
void ExecuteJobsCore() |
|||
{ |
|||
long? backgroundJobExecutionStartedAt = null; |
|||
while (true) |
|||
{ |
|||
DispatcherOperation? job; |
|||
|
|||
lock (InstanceLock) |
|||
job = _queue.Peek(); |
|||
|
|||
if (job == null || job.Priority < DispatcherPriority.MinimumActiveValue) |
|||
return; |
|||
|
|||
|
|||
// We don't stop for executing jobs queued with >Input priority
|
|||
if (job.Priority > DispatcherPriority.Input) |
|||
{ |
|||
ExecuteJob(job); |
|||
backgroundJobExecutionStartedAt = null; |
|||
} |
|||
// If platform supports pending input query, ask the platform if we can continue running low priority jobs
|
|||
else if (_pendingInputImpl?.CanQueryPendingInput == true) |
|||
{ |
|||
if (!_pendingInputImpl.HasPendingInput) |
|||
ExecuteJob(job); |
|||
else |
|||
{ |
|||
RequestBackgroundProcessing(); |
|||
return; |
|||
} |
|||
} |
|||
// We can't check if there is pending input, but still need to enforce interactivity
|
|||
// so we stop processing background jobs after some timeout and start a timer to continue later
|
|||
else |
|||
{ |
|||
if (backgroundJobExecutionStartedAt == null) |
|||
backgroundJobExecutionStartedAt = Now; |
|||
|
|||
if (Now - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) |
|||
{ |
|||
_signaled = true; |
|||
RequestBackgroundProcessing(); |
|||
return; |
|||
} |
|||
else |
|||
ExecuteJob(job); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private bool RequestProcessing() |
|||
{ |
|||
lock (InstanceLock) |
|||
{ |
|||
if (!CheckAccess()) |
|||
{ |
|||
RequestForegroundProcessing(); |
|||
return true; |
|||
} |
|||
|
|||
if (_queue.MaxPriority <= DispatcherPriority.Input) |
|||
{ |
|||
if (_pendingInputImpl is { CanQueryPendingInput: true, HasPendingInput: false }) |
|||
RequestForegroundProcessing(); |
|||
else |
|||
RequestBackgroundProcessing(); |
|||
} |
|||
else |
|||
RequestForegroundProcessing(); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
private void RequestForegroundProcessing() |
|||
{ |
|||
if (!_signaled) |
|||
{ |
|||
_signaled = true; |
|||
_impl.Signal(); |
|||
} |
|||
} |
|||
|
|||
internal void Abort(DispatcherOperation operation) |
|||
{ |
|||
lock (InstanceLock) |
|||
_queue.RemoveItem(operation); |
|||
operation.DoAbort(); |
|||
} |
|||
|
|||
// Returns whether or not the priority was set.
|
|||
internal bool SetPriority(DispatcherOperation operation, DispatcherPriority priority) // NOTE: should be Priority
|
|||
{ |
|||
bool notify = false; |
|||
|
|||
lock(InstanceLock) |
|||
{ |
|||
if(operation.IsQueued) |
|||
{ |
|||
_queue.ChangeItemPriority(operation, priority); |
|||
notify = true; |
|||
|
|||
if(notify) |
|||
{ |
|||
// Make sure we will wake up to process this operation.
|
|||
RequestProcessing(); |
|||
|
|||
} |
|||
} |
|||
} |
|||
return notify; |
|||
} |
|||
|
|||
public bool HasJobsWithPriority(DispatcherPriority priority) |
|||
{ |
|||
lock (InstanceLock) |
|||
return _queue.MaxPriority >= priority; |
|||
} |
|||
} |
|||
@ -0,0 +1,207 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Threading; |
|||
|
|||
public partial class Dispatcher |
|||
{ |
|||
private List<DispatcherTimer> _timers = new(); |
|||
private long _timersVersion; |
|||
private bool _dueTimeFound; |
|||
private long _dueTimeInMs; |
|||
|
|||
private long? _dueTimeForTimers; |
|||
private long? _dueTimeForBackgroundProcessing; |
|||
private long? _osTimerSetTo; |
|||
|
|||
internal long Now => _impl.Now; |
|||
|
|||
private void UpdateOSTimer() |
|||
{ |
|||
VerifyAccess(); |
|||
var nextDueTime = |
|||
(_dueTimeForTimers.HasValue && _dueTimeForBackgroundProcessing.HasValue) ? |
|||
Math.Min(_dueTimeForTimers.Value, _dueTimeForBackgroundProcessing.Value) : |
|||
_dueTimeForTimers ?? _dueTimeForBackgroundProcessing; |
|||
if (_osTimerSetTo == nextDueTime) |
|||
return; |
|||
_impl.UpdateTimer(_osTimerSetTo = nextDueTime); |
|||
} |
|||
|
|||
internal void RescheduleTimers() |
|||
{ |
|||
if (!CheckAccess()) |
|||
{ |
|||
Post(RescheduleTimers, DispatcherPriority.Send); |
|||
return; |
|||
} |
|||
|
|||
lock (InstanceLock) |
|||
{ |
|||
if (!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read
|
|||
{ |
|||
bool oldDueTimeFound = _dueTimeFound; |
|||
long oldDueTimeInTicks = _dueTimeInMs; |
|||
_dueTimeFound = false; |
|||
_dueTimeInMs = 0; |
|||
|
|||
if (_timers.Count > 0) |
|||
{ |
|||
// We could do better if we sorted the list of timers.
|
|||
for (int i = 0; i < _timers.Count; i++) |
|||
{ |
|||
var timer = _timers[i]; |
|||
|
|||
if (!_dueTimeFound || timer.DueTimeInMs - _dueTimeInMs < 0) |
|||
{ |
|||
_dueTimeFound = true; |
|||
_dueTimeInMs = timer.DueTimeInMs; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (_dueTimeFound) |
|||
{ |
|||
if (_dueTimeForTimers == null || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInMs)) |
|||
{ |
|||
_dueTimeForTimers = _dueTimeInMs; |
|||
UpdateOSTimer(); |
|||
} |
|||
} |
|||
else if (oldDueTimeFound) |
|||
{ |
|||
_dueTimeForTimers = null; |
|||
UpdateOSTimer(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal void AddTimer(DispatcherTimer timer) |
|||
{ |
|||
lock (InstanceLock) |
|||
{ |
|||
if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
|
|||
{ |
|||
_timers.Add(timer); |
|||
_timersVersion++; |
|||
} |
|||
} |
|||
|
|||
RescheduleTimers(); |
|||
} |
|||
|
|||
internal void RemoveTimer(DispatcherTimer timer) |
|||
{ |
|||
lock (InstanceLock) |
|||
{ |
|||
if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
|
|||
{ |
|||
_timers.Remove(timer); |
|||
_timersVersion++; |
|||
} |
|||
} |
|||
|
|||
RescheduleTimers(); |
|||
} |
|||
|
|||
private void OnOSTimer() |
|||
{ |
|||
_impl.UpdateTimer(null); |
|||
_osTimerSetTo = null; |
|||
bool needToPromoteTimers = false; |
|||
bool needToProcessQueue = false; |
|||
lock (InstanceLock) |
|||
{ |
|||
_impl.UpdateTimer(_osTimerSetTo = null); |
|||
needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now; |
|||
if (needToPromoteTimers) |
|||
_dueTimeForTimers = null; |
|||
needToProcessQueue = _dueTimeForBackgroundProcessing.HasValue && |
|||
_dueTimeForBackgroundProcessing.Value <= Now; |
|||
if (needToProcessQueue) |
|||
_dueTimeForBackgroundProcessing = null; |
|||
} |
|||
|
|||
if (needToPromoteTimers) |
|||
PromoteTimers(); |
|||
if (needToProcessQueue) |
|||
ExecuteJobsCore(); |
|||
UpdateOSTimer(); |
|||
} |
|||
|
|||
internal void PromoteTimers() |
|||
{ |
|||
long currentTimeInTicks = Now; |
|||
try |
|||
{ |
|||
List<DispatcherTimer>? timers = null; |
|||
long timersVersion = 0; |
|||
|
|||
lock (InstanceLock) |
|||
{ |
|||
if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
|
|||
{ |
|||
if (_dueTimeFound && _dueTimeInMs - currentTimeInTicks <= 0) |
|||
{ |
|||
timers = _timers; |
|||
timersVersion = _timersVersion; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (timers != null) |
|||
{ |
|||
DispatcherTimer? timer = null; |
|||
int iTimer = 0; |
|||
|
|||
do |
|||
{ |
|||
lock (InstanceLock) |
|||
{ |
|||
timer = null; |
|||
|
|||
// If the timers collection changed while we are in the middle of
|
|||
// looking for timers, start over.
|
|||
if (timersVersion != _timersVersion) |
|||
{ |
|||
timersVersion = _timersVersion; |
|||
iTimer = 0; |
|||
} |
|||
|
|||
while (iTimer < _timers.Count) |
|||
{ |
|||
// WARNING: this is vulnerable to wrapping
|
|||
if (timers[iTimer].DueTimeInMs - currentTimeInTicks <= 0) |
|||
{ |
|||
// Remove this timer from our list.
|
|||
// Do not increment the index.
|
|||
timer = timers[iTimer]; |
|||
timers.RemoveAt(iTimer); |
|||
break; |
|||
} |
|||
else |
|||
{ |
|||
iTimer++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Now that we are outside of the lock, promote the timer.
|
|||
if (timer != null) |
|||
{ |
|||
timer.Promote(); |
|||
} |
|||
} while (timer != null); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
RescheduleTimers(); |
|||
} |
|||
} |
|||
|
|||
internal static List<DispatcherTimer> SnapshotTimersForUnitTests() => |
|||
s_uiThread!._timers.ToList(); |
|||
} |
|||
@ -1,159 +1,121 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Threading |
|||
namespace Avalonia.Threading; |
|||
|
|||
/// <summary>
|
|||
/// Provides services for managing work items on a thread.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// In Avalonia, there is usually only a single <see cref="Dispatcher"/> in the application -
|
|||
/// the one for the UI thread, retrieved via the <see cref="UIThread"/> property.
|
|||
/// </remarks>
|
|||
public partial class Dispatcher : IDispatcher |
|||
{ |
|||
/// <summary>
|
|||
/// Provides services for managing work items on a thread.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// In Avalonia, there is usually only a single <see cref="Dispatcher"/> in the application -
|
|||
/// the one for the UI thread, retrieved via the <see cref="UIThread"/> property.
|
|||
/// </remarks>
|
|||
public class Dispatcher : IDispatcher |
|||
private IDispatcherImpl _impl; |
|||
internal object InstanceLock { get; } = new(); |
|||
private bool _hasShutdownFinished; |
|||
private IControlledDispatcherImpl? _controlledImpl; |
|||
private static Dispatcher? s_uiThread; |
|||
private IDispatcherImplWithPendingInput? _pendingInputImpl; |
|||
private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; |
|||
|
|||
internal Dispatcher(IDispatcherImpl impl) |
|||
{ |
|||
private readonly JobRunner _jobRunner; |
|||
private IPlatformThreadingInterface? _platform; |
|||
|
|||
public static Dispatcher UIThread { get; } = |
|||
new Dispatcher(AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>()); |
|||
|
|||
public Dispatcher(IPlatformThreadingInterface? platform) |
|||
{ |
|||
_platform = platform; |
|||
_jobRunner = new JobRunner(platform); |
|||
|
|||
if (_platform != null) |
|||
{ |
|||
_platform.Signaled += _jobRunner.RunJobs; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Checks that the current thread is the UI thread.
|
|||
/// </summary>
|
|||
public bool CheckAccess() => _platform?.CurrentThreadIsLoopThread ?? true; |
|||
|
|||
/// <summary>
|
|||
/// Checks that the current thread is the UI thread and throws if not.
|
|||
/// </summary>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// The current thread is not the UI thread.
|
|||
/// </exception>
|
|||
public void VerifyAccess() |
|||
{ |
|||
if (!CheckAccess()) |
|||
throw new InvalidOperationException("Call from invalid thread"); |
|||
} |
|||
|
|||
/// <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) |
|||
{ |
|||
var platform = AvaloniaLocator.Current.GetRequiredService<IPlatformThreadingInterface>(); |
|||
cancellationToken.Register(() => platform.Signal(DispatcherPriority.Send)); |
|||
platform.RunLoop(cancellationToken); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Runs continuations pushed on the loop.
|
|||
/// </summary>
|
|||
public void RunJobs() |
|||
{ |
|||
_jobRunner.RunJobs(null); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Use this method to ensure that more prioritized tasks are executed
|
|||
/// </summary>
|
|||
/// <param name="minimumPriority"></param>
|
|||
public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority); |
|||
|
|||
/// <summary>
|
|||
/// Use this method to check if there are more prioritized tasks
|
|||
/// </summary>
|
|||
/// <param name="minimumPriority"></param>
|
|||
public bool HasJobsWithPriority(DispatcherPriority minimumPriority) => |
|||
_jobRunner.HasJobsWithPriority(minimumPriority); |
|||
|
|||
/// <inheritdoc/>
|
|||
public Task InvokeAsync(Action action, DispatcherPriority priority = default) |
|||
{ |
|||
_ = action ?? throw new ArgumentNullException(nameof(action)); |
|||
return _jobRunner.InvokeAsync(action, priority); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority = default) |
|||
{ |
|||
_ = function ?? throw new ArgumentNullException(nameof(function)); |
|||
return _jobRunner.InvokeAsync(function, priority); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Task InvokeAsync(Func<Task> function, DispatcherPriority priority = default) |
|||
{ |
|||
_ = function ?? throw new ArgumentNullException(nameof(function)); |
|||
return _jobRunner.InvokeAsync(function, priority).Unwrap(); |
|||
} |
|||
_impl = impl; |
|||
impl.Timer += OnOSTimer; |
|||
impl.Signaled += Signaled; |
|||
_controlledImpl = _impl as IControlledDispatcherImpl; |
|||
_pendingInputImpl = _impl as IDispatcherImplWithPendingInput; |
|||
_backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; |
|||
if (_backgroundProcessingImpl != null) |
|||
_backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; |
|||
} |
|||
|
|||
public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority = default) |
|||
private static Dispatcher CreateUIThreadDispatcher() |
|||
{ |
|||
var impl = AvaloniaLocator.Current.GetService<IDispatcherImpl>(); |
|||
if (impl == null) |
|||
{ |
|||
_ = function ?? throw new ArgumentNullException(nameof(function)); |
|||
return _jobRunner.InvokeAsync(function, priority).Unwrap(); |
|||
var platformThreading = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>(); |
|||
if (platformThreading != null) |
|||
impl = new LegacyDispatcherImpl(platformThreading); |
|||
else |
|||
impl = new NullDispatcherImpl(); |
|||
} |
|||
return new Dispatcher(impl); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Post(Action action, DispatcherPriority priority = default) |
|||
{ |
|||
_ = action ?? throw new ArgumentNullException(nameof(action)); |
|||
_jobRunner.Post(action, priority); |
|||
} |
|||
/// <summary>
|
|||
/// Checks that the current thread is the UI thread.
|
|||
/// </summary>
|
|||
public bool CheckAccess() => _impl?.CurrentThreadIsLoopThread ?? true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default) |
|||
/// <summary>
|
|||
/// Checks that the current thread is the UI thread and throws if not.
|
|||
/// </summary>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// The current thread is not the UI thread.
|
|||
/// </exception>
|
|||
public void VerifyAccess() |
|||
{ |
|||
if (!CheckAccess()) |
|||
{ |
|||
_ = action ?? throw new ArgumentNullException(nameof(action)); |
|||
_jobRunner.Post(action, arg, priority); |
|||
} |
|||
// Used to inline VerifyAccess.
|
|||
[DoesNotReturn] |
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
static void ThrowVerifyAccess() |
|||
=> throw new InvalidOperationException("Call from invalid thread"); |
|||
|
|||
/// <summary>
|
|||
/// This is needed for platform backends that don't have internal priority system (e. g. win32)
|
|||
/// To ensure that there are no jobs with higher priority
|
|||
/// </summary>
|
|||
/// <param name="currentPriority"></param>
|
|||
internal void EnsurePriority(DispatcherPriority currentPriority) |
|||
{ |
|||
if (currentPriority == DispatcherPriority.MaxValue) |
|||
return; |
|||
currentPriority += 1; |
|||
_jobRunner.RunJobs(currentPriority); |
|||
ThrowVerifyAccess(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Allows unit tests to change the platform threading interface.
|
|||
/// </summary>
|
|||
internal void UpdateServices() |
|||
internal void Shutdown() |
|||
{ |
|||
DispatcherOperation? operation = null; |
|||
_impl.Timer -= PromoteTimers; |
|||
_impl.Signaled -= Signaled; |
|||
do |
|||
{ |
|||
if (_platform != null) |
|||
lock(InstanceLock) |
|||
{ |
|||
_platform.Signaled -= _jobRunner.RunJobs; |
|||
if(_queue.MaxPriority != DispatcherPriority.Invalid) |
|||
{ |
|||
operation = _queue.Peek(); |
|||
} |
|||
else |
|||
{ |
|||
operation = null; |
|||
} |
|||
} |
|||
|
|||
_platform = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>(); |
|||
_jobRunner.UpdateServices(); |
|||
|
|||
if (_platform != null) |
|||
if(operation != null) |
|||
{ |
|||
_platform.Signaled += _jobRunner.RunJobs; |
|||
operation.Abort(); |
|||
} |
|||
} |
|||
} while(operation != null); |
|||
_impl.UpdateTimer(null); |
|||
_hasShutdownFinished = true; |
|||
} |
|||
|
|||
/// <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(); |
|||
cancellationToken.Register(() => RequestProcessing()); |
|||
_controlledImpl.RunLoop(cancellationToken); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,308 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Threading; |
|||
|
|||
public class DispatcherOperation |
|||
{ |
|||
protected readonly bool ThrowOnUiThread; |
|||
public DispatcherOperationStatus Status { get; protected set; } |
|||
public Dispatcher Dispatcher { get; } |
|||
|
|||
public DispatcherPriority Priority |
|||
{ |
|||
get => _priority; |
|||
set |
|||
{ |
|||
_priority = value; |
|||
// Dispatcher is null in ctor
|
|||
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
|
|||
Dispatcher?.SetPriority(this, value); |
|||
} |
|||
} |
|||
|
|||
protected object? Callback; |
|||
protected object? TaskSource; |
|||
|
|||
internal DispatcherOperation? SequentialPrev { get; set; } |
|||
internal DispatcherOperation? SequentialNext { get; set; } |
|||
internal DispatcherOperation? PriorityPrev { get; set; } |
|||
internal DispatcherOperation? PriorityNext { get; set; } |
|||
internal PriorityChain? Chain { get; set; } |
|||
|
|||
internal bool IsQueued => Chain != null; |
|||
|
|||
private EventHandler? _aborted; |
|||
private EventHandler? _completed; |
|||
private DispatcherPriority _priority; |
|||
|
|||
internal DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Action callback, bool throwOnUiThread) : |
|||
this(dispatcher, priority, throwOnUiThread) |
|||
{ |
|||
Callback = callback; |
|||
} |
|||
|
|||
private protected DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, bool throwOnUiThread) |
|||
{ |
|||
ThrowOnUiThread = throwOnUiThread; |
|||
Priority = priority; |
|||
Dispatcher = dispatcher; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// An event that is raised when the operation is aborted or canceled.
|
|||
/// </summary>
|
|||
public event EventHandler Aborted |
|||
{ |
|||
add |
|||
{ |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
_aborted += value; |
|||
} |
|||
} |
|||
|
|||
remove |
|||
{ |
|||
lock(Dispatcher.InstanceLock) |
|||
{ |
|||
_aborted -= value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// An event that is raised when the operation completes.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Completed indicates that the operation was invoked and has
|
|||
/// either completed successfully or faulted. Note that a canceled
|
|||
/// or aborted operation is never is never considered completed.
|
|||
/// </remarks>
|
|||
public event EventHandler Completed |
|||
{ |
|||
add |
|||
{ |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
_completed += value; |
|||
} |
|||
} |
|||
|
|||
remove |
|||
{ |
|||
lock(Dispatcher.InstanceLock) |
|||
{ |
|||
_completed -= value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Abort() |
|||
{ |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
if (Status == DispatcherOperationStatus.Pending) |
|||
return; |
|||
Dispatcher.Abort(this); |
|||
} |
|||
} |
|||
|
|||
public void Wait() |
|||
{ |
|||
if (Dispatcher.CheckAccess()) |
|||
throw new InvalidOperationException("Wait is only supported on background thread"); |
|||
GetTask().Wait(); |
|||
} |
|||
|
|||
public Task GetTask() => GetTaskCore(); |
|||
|
|||
/// <summary>
|
|||
/// Returns an awaiter for awaiting the completion of the operation.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method is intended to be used by compilers.
|
|||
/// </remarks>
|
|||
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] |
|||
public TaskAwaiter GetAwaiter() |
|||
{ |
|||
return GetTask().GetAwaiter(); |
|||
} |
|||
|
|||
internal void DoAbort() |
|||
{ |
|||
Status = DispatcherOperationStatus.Aborted; |
|||
AbortTask(); |
|||
_aborted?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
|
|||
internal void Execute() |
|||
{ |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
Status = DispatcherOperationStatus.Executing; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
InvokeCore(); |
|||
} |
|||
finally |
|||
{ |
|||
_completed?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
|
|||
protected virtual void InvokeCore() |
|||
{ |
|||
try |
|||
{ |
|||
((Action)Callback!)(); |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
Status = DispatcherOperationStatus.Completed; |
|||
if (TaskSource is TaskCompletionSource<object?> tcs) |
|||
tcs.SetResult(null); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
Status = DispatcherOperationStatus.Completed; |
|||
if (TaskSource is TaskCompletionSource<object?> tcs) |
|||
tcs.SetException(e); |
|||
} |
|||
|
|||
if (ThrowOnUiThread) |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
internal virtual object? GetResult() => null; |
|||
|
|||
protected virtual void AbortTask() => (TaskSource as TaskCompletionSource<object?>)?.SetCanceled(); |
|||
|
|||
private static CancellationToken CreateCancelledToken() |
|||
{ |
|||
var cts = new CancellationTokenSource(); |
|||
cts.Cancel(); |
|||
return cts.Token; |
|||
} |
|||
|
|||
private static readonly Task s_abortedTask = Task.FromCanceled(CreateCancelledToken()); |
|||
|
|||
protected virtual Task GetTaskCore() |
|||
{ |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
if (Status == DispatcherOperationStatus.Aborted) |
|||
return s_abortedTask; |
|||
if (Status == DispatcherOperationStatus.Completed) |
|||
return Task.CompletedTask; |
|||
if (TaskSource is not TaskCompletionSource<object?> tcs) |
|||
TaskSource = tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); |
|||
return tcs.Task; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public class DispatcherOperation<T> : DispatcherOperation |
|||
{ |
|||
public DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Func<T> callback) : base(dispatcher, priority, false) |
|||
{ |
|||
TaskSource = new TaskCompletionSource<T>(); |
|||
Callback = callback; |
|||
} |
|||
|
|||
private TaskCompletionSource<T> TaskCompletionSource => (TaskCompletionSource<T>)TaskSource!; |
|||
|
|||
public new Task<T> GetTask() => TaskCompletionSource!.Task; |
|||
|
|||
protected override Task GetTaskCore() => GetTask(); |
|||
|
|||
protected override void AbortTask() => TaskCompletionSource.SetCanceled(); |
|||
|
|||
internal override object? GetResult() => GetTask().Result; |
|||
|
|||
protected override void InvokeCore() |
|||
{ |
|||
try |
|||
{ |
|||
var result = ((Func<T>)Callback!)(); |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
Status = DispatcherOperationStatus.Completed; |
|||
TaskCompletionSource.SetResult(result); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
Status = DispatcherOperationStatus.Completed; |
|||
TaskCompletionSource.SetException(e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public T Result |
|||
{ |
|||
get |
|||
{ |
|||
if (TaskCompletionSource.Task.IsCompleted || !Dispatcher.CheckAccess()) |
|||
return TaskCompletionSource.Task.GetAwaiter().GetResult(); |
|||
throw new InvalidOperationException("Synchronous wait is only supported on non-UI threads"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal class SendOrPostCallbackDispatcherOperation : DispatcherOperation |
|||
{ |
|||
private readonly object? _arg; |
|||
|
|||
internal SendOrPostCallbackDispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, |
|||
SendOrPostCallback callback, object? arg, bool throwOnUiThread) |
|||
: base(dispatcher, priority, throwOnUiThread) |
|||
{ |
|||
Callback = callback; |
|||
_arg = arg; |
|||
} |
|||
|
|||
protected override void InvokeCore() |
|||
{ |
|||
try |
|||
{ |
|||
((SendOrPostCallback)Callback!)(_arg); |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
Status = DispatcherOperationStatus.Completed; |
|||
if (TaskSource is TaskCompletionSource<object?> tcs) |
|||
tcs.SetResult(null); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
lock (Dispatcher.InstanceLock) |
|||
{ |
|||
Status = DispatcherOperationStatus.Completed; |
|||
if (TaskSource is TaskCompletionSource<object?> tcs) |
|||
tcs.SetException(e); |
|||
} |
|||
|
|||
if (ThrowOnUiThread) |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public enum DispatcherOperationStatus |
|||
{ |
|||
Pending = 0, |
|||
Aborted = 1, |
|||
Completed = 2, |
|||
Executing = 3, |
|||
} |
|||
@ -0,0 +1,418 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Threading; |
|||
|
|||
|
|||
internal class DispatcherPriorityQueue |
|||
{ |
|||
// Priority chains...
|
|||
private readonly SortedList<int, PriorityChain> _priorityChains; // NOTE: should be Priority
|
|||
private readonly Stack<PriorityChain> _cacheReusableChains; |
|||
|
|||
// Sequential chain...
|
|||
private DispatcherOperation? _head; |
|||
private DispatcherOperation? _tail; |
|||
|
|||
public DispatcherPriorityQueue() |
|||
{ |
|||
// Build the collection of priority chains.
|
|||
_priorityChains = new SortedList<int, PriorityChain>(); // NOTE: should be Priority
|
|||
_cacheReusableChains = new Stack<PriorityChain>(10); |
|||
|
|||
_head = _tail = null; |
|||
} |
|||
|
|||
// NOTE: not used
|
|||
// public int Count {get{return _count;}}
|
|||
|
|||
public DispatcherPriority MaxPriority // NOTE: should be Priority
|
|||
{ |
|||
get |
|||
{ |
|||
int count = _priorityChains.Count; |
|||
|
|||
if (count > 0) |
|||
{ |
|||
return _priorityChains.Keys[count - 1]; |
|||
} |
|||
else |
|||
{ |
|||
return DispatcherPriority.Invalid; // NOTE: should be Priority.Invalid;
|
|||
} |
|||
} |
|||
} |
|||
|
|||
public DispatcherOperation Enqueue(DispatcherPriority priority, DispatcherOperation item) // NOTE: should be Priority
|
|||
{ |
|||
// Find the existing chain for this priority, or create a new one
|
|||
// if one does not exist.
|
|||
PriorityChain chain = GetChain(priority); |
|||
|
|||
// Step 1: Append this to the end of the "sequential" linked list.
|
|||
InsertItemInSequentialChain(item, _tail); |
|||
|
|||
// Step 2: Append the item into the priority chain.
|
|||
InsertItemInPriorityChain(item, chain, chain.Tail); |
|||
|
|||
return item; |
|||
} |
|||
|
|||
public DispatcherOperation Dequeue() |
|||
{ |
|||
// Get the max-priority chain.
|
|||
int count = _priorityChains.Count; |
|||
if (count > 0) |
|||
{ |
|||
PriorityChain chain = _priorityChains.Values[count - 1]; |
|||
Debug.Assert(chain != null, "PriorityQueue.Dequeue: a chain should exist."); |
|||
|
|||
DispatcherOperation? item = chain.Head; |
|||
Debug.Assert(item != null, "PriorityQueue.Dequeue: a priority item should exist."); |
|||
|
|||
RemoveItem(item); |
|||
|
|||
return item; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException(); |
|||
} |
|||
} |
|||
|
|||
public DispatcherOperation? Peek() |
|||
{ |
|||
// Get the max-priority chain.
|
|||
int count = _priorityChains.Count; |
|||
if (count > 0) |
|||
{ |
|||
PriorityChain chain = _priorityChains.Values[count - 1]; |
|||
Debug.Assert(chain != null, "PriorityQueue.Peek: a chain should exist."); |
|||
|
|||
DispatcherOperation? item = chain.Head; |
|||
Debug.Assert(item != null, "PriorityQueue.Peek: a priority item should exist."); |
|||
|
|||
return item; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public void RemoveItem(DispatcherOperation item) |
|||
{ |
|||
Debug.Assert(item != null, "PriorityQueue.RemoveItem: invalid item."); |
|||
Debug.Assert(item.Chain != null, "PriorityQueue.RemoveItem: a chain should exist."); |
|||
|
|||
// Step 1: Remove the item from its priority chain.
|
|||
RemoveItemFromPriorityChain(item); |
|||
|
|||
// Step 2: Remove the item from the sequential chain.
|
|||
RemoveItemFromSequentialChain(item); |
|||
} |
|||
|
|||
public void ChangeItemPriority(DispatcherOperation item, DispatcherPriority priority) // NOTE: should be Priority
|
|||
{ |
|||
// Remove the item from its current priority and insert it into
|
|||
// the new priority chain. Note that this does not change the
|
|||
// sequential ordering.
|
|||
|
|||
// Step 1: Remove the item from the priority chain.
|
|||
RemoveItemFromPriorityChain(item); |
|||
|
|||
// Step 2: Insert the item into the new priority chain.
|
|||
// Find the existing chain for this priority, or create a new one
|
|||
// if one does not exist.
|
|||
PriorityChain chain = GetChain(priority); |
|||
InsertItemInPriorityChain(item, chain); |
|||
} |
|||
|
|||
private PriorityChain GetChain(DispatcherPriority priority) // NOTE: should be Priority
|
|||
{ |
|||
PriorityChain? chain = null; |
|||
|
|||
int count = _priorityChains.Count; |
|||
if (count > 0) |
|||
{ |
|||
if (priority == _priorityChains.Keys[0]) |
|||
{ |
|||
chain = _priorityChains.Values[0]; |
|||
} |
|||
else if (priority == _priorityChains.Keys[count - 1]) |
|||
{ |
|||
chain = _priorityChains.Values[count - 1]; |
|||
} |
|||
else if ((priority > _priorityChains.Keys[0]) && |
|||
(priority < _priorityChains.Keys[count - 1])) |
|||
{ |
|||
_priorityChains.TryGetValue(priority, out chain); |
|||
} |
|||
} |
|||
|
|||
if (chain == null) |
|||
{ |
|||
if (_cacheReusableChains.Count > 0) |
|||
{ |
|||
chain = _cacheReusableChains.Pop(); |
|||
chain.Priority = priority; |
|||
} |
|||
else |
|||
{ |
|||
chain = new PriorityChain(priority); |
|||
} |
|||
|
|||
_priorityChains.Add(priority, chain); |
|||
} |
|||
|
|||
return chain; |
|||
} |
|||
|
|||
private void InsertItemInPriorityChain(DispatcherOperation item, PriorityChain chain) |
|||
{ |
|||
// Scan along the sequential chain, in the previous direction,
|
|||
// looking for an item that is already in the new chain. We will
|
|||
// insert ourselves after the item we found. We can short-circuit
|
|||
// this search if the new chain is empty.
|
|||
if (chain.Head == null) |
|||
{ |
|||
Debug.Assert(chain.Tail == null, |
|||
"PriorityQueue.InsertItemInPriorityChain: both the head and the tail should be null."); |
|||
InsertItemInPriorityChain(item, chain, null); |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(chain.Tail != null, |
|||
"PriorityQueue.InsertItemInPriorityChain: both the head and the tail should not be null."); |
|||
|
|||
DispatcherOperation? after; |
|||
|
|||
// Search backwards along the sequential chain looking for an
|
|||
// item already in this list.
|
|||
for (after = item.SequentialPrev; after != null; after = after.SequentialPrev) |
|||
{ |
|||
if (after.Chain == chain) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
InsertItemInPriorityChain(item, chain, after); |
|||
} |
|||
} |
|||
|
|||
internal void InsertItemInPriorityChain(DispatcherOperation item, PriorityChain chain, DispatcherOperation? after) |
|||
{ |
|||
Debug.Assert(chain != null, "PriorityQueue.InsertItemInPriorityChain: a chain must be provided."); |
|||
Debug.Assert(item.Chain == null && item.PriorityPrev == null && item.PriorityNext == null, |
|||
"PriorityQueue.InsertItemInPriorityChain: item must not already be in a priority chain."); |
|||
|
|||
item.Chain = chain; |
|||
|
|||
if (after == null) |
|||
{ |
|||
// Note: passing null for after means insert at the head.
|
|||
|
|||
if (chain.Head != null) |
|||
{ |
|||
Debug.Assert(chain.Tail != null, |
|||
"PriorityQueue.InsertItemInPriorityChain: both the head and the tail should not be null."); |
|||
|
|||
chain.Head.PriorityPrev = item; |
|||
item.PriorityNext = chain.Head; |
|||
chain.Head = item; |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(chain.Tail == null, |
|||
"PriorityQueue.InsertItemInPriorityChain: both the head and the tail should be null."); |
|||
|
|||
chain.Head = chain.Tail = item; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
item.PriorityPrev = after; |
|||
|
|||
if (after.PriorityNext != null) |
|||
{ |
|||
item.PriorityNext = after.PriorityNext; |
|||
after.PriorityNext.PriorityPrev = item; |
|||
after.PriorityNext = item; |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(item.Chain.Tail == after, |
|||
"PriorityQueue.InsertItemInPriorityChain: the chain's tail should be the item we are inserting after."); |
|||
after.PriorityNext = item; |
|||
chain.Tail = item; |
|||
} |
|||
} |
|||
|
|||
chain.Count++; |
|||
} |
|||
|
|||
private void RemoveItemFromPriorityChain(DispatcherOperation item) |
|||
{ |
|||
Debug.Assert(item != null, "PriorityQueue.RemoveItemFromPriorityChain: invalid item."); |
|||
Debug.Assert(item.Chain != null, "PriorityQueue.RemoveItemFromPriorityChain: a chain should exist."); |
|||
|
|||
// Step 1: Fix up the previous link
|
|||
if (item.PriorityPrev != null) |
|||
{ |
|||
Debug.Assert(item.Chain.Head != item, |
|||
"PriorityQueue.RemoveItemFromPriorityChain: the head should not point to this item."); |
|||
|
|||
item.PriorityPrev.PriorityNext = item.PriorityNext; |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(item.Chain.Head == item, |
|||
"PriorityQueue.RemoveItemFromPriorityChain: the head should point to this item."); |
|||
|
|||
item.Chain.Head = item.PriorityNext; |
|||
} |
|||
|
|||
// Step 2: Fix up the next link
|
|||
if (item.PriorityNext != null) |
|||
{ |
|||
Debug.Assert(item.Chain.Tail != item, |
|||
"PriorityQueue.RemoveItemFromPriorityChain: the tail should not point to this item."); |
|||
|
|||
item.PriorityNext.PriorityPrev = item.PriorityPrev; |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(item.Chain.Tail == item, |
|||
"PriorityQueue.RemoveItemFromPriorityChain: the tail should point to this item."); |
|||
|
|||
item.Chain.Tail = item.PriorityPrev; |
|||
} |
|||
|
|||
// Step 3: cleanup
|
|||
item.PriorityPrev = item.PriorityNext = null; |
|||
item.Chain.Count--; |
|||
if (item.Chain.Count == 0) |
|||
{ |
|||
if (item.Chain.Priority == _priorityChains.Keys[_priorityChains.Count - 1]) |
|||
{ |
|||
_priorityChains.RemoveAt(_priorityChains.Count - 1); |
|||
} |
|||
else |
|||
{ |
|||
_priorityChains.Remove(item.Chain.Priority); |
|||
} |
|||
|
|||
if (_cacheReusableChains.Count < 10) |
|||
{ |
|||
item.Chain.Priority = DispatcherPriority.Invalid; |
|||
_cacheReusableChains.Push(item.Chain); |
|||
} |
|||
} |
|||
|
|||
item.Chain = null; |
|||
} |
|||
|
|||
internal void InsertItemInSequentialChain(DispatcherOperation item, DispatcherOperation? after) |
|||
{ |
|||
Debug.Assert(item.SequentialPrev == null && item.SequentialNext == null, |
|||
"PriorityQueue.InsertItemInSequentialChain: item must not already be in the sequential chain."); |
|||
|
|||
if (after == null) |
|||
{ |
|||
// Note: passing null for after means insert at the head.
|
|||
|
|||
if (_head != null) |
|||
{ |
|||
Debug.Assert(_tail != null, |
|||
"PriorityQueue.InsertItemInSequentialChain: both the head and the tail should not be null."); |
|||
|
|||
_head.SequentialPrev = item; |
|||
item.SequentialNext = _head; |
|||
_head = item; |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(_tail == null, |
|||
"PriorityQueue.InsertItemInSequentialChain: both the head and the tail should be null."); |
|||
|
|||
_head = _tail = item; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
item.SequentialPrev = after; |
|||
|
|||
if (after.SequentialNext != null) |
|||
{ |
|||
item.SequentialNext = after.SequentialNext; |
|||
after.SequentialNext.SequentialPrev = item; |
|||
after.SequentialNext = item; |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(_tail == after, |
|||
"PriorityQueue.InsertItemInSequentialChain: the tail should be the item we are inserting after."); |
|||
after.SequentialNext = item; |
|||
_tail = item; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void RemoveItemFromSequentialChain(DispatcherOperation item) |
|||
{ |
|||
Debug.Assert(item != null, "PriorityQueue.RemoveItemFromSequentialChain: invalid item."); |
|||
|
|||
// Step 1: Fix up the previous link
|
|||
if (item.SequentialPrev != null) |
|||
{ |
|||
Debug.Assert(_head != item, |
|||
"PriorityQueue.RemoveItemFromSequentialChain: the head should not point to this item."); |
|||
|
|||
item.SequentialPrev.SequentialNext = item.SequentialNext; |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(_head == item, |
|||
"PriorityQueue.RemoveItemFromSequentialChain: the head should point to this item."); |
|||
|
|||
_head = item.SequentialNext; |
|||
} |
|||
|
|||
// Step 2: Fix up the next link
|
|||
if (item.SequentialNext != null) |
|||
{ |
|||
Debug.Assert(_tail != item, |
|||
"PriorityQueue.RemoveItemFromSequentialChain: the tail should not point to this item."); |
|||
|
|||
item.SequentialNext.SequentialPrev = item.SequentialPrev; |
|||
} |
|||
else |
|||
{ |
|||
Debug.Assert(_tail == item, |
|||
"PriorityQueue.RemoveItemFromSequentialChain: the tail should point to this item."); |
|||
|
|||
_tail = item.SequentialPrev; |
|||
} |
|||
|
|||
// Step 3: cleanup
|
|||
item.SequentialPrev = item.SequentialNext = null; |
|||
} |
|||
} |
|||
|
|||
|
|||
internal class PriorityChain |
|||
{ |
|||
public PriorityChain(DispatcherPriority priority) // NOTE: should be Priority
|
|||
{ |
|||
Priority = priority; |
|||
} |
|||
|
|||
public DispatcherPriority Priority { get; set; } // NOTE: should be Priority
|
|||
|
|||
public int Count { get; set; } |
|||
|
|||
public DispatcherOperation? Head { get; set; } |
|||
|
|||
public DispatcherOperation? Tail { get; set; } |
|||
} |
|||
@ -1,207 +1,352 @@ |
|||
using System; |
|||
using Avalonia.Reactive; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Threading |
|||
namespace Avalonia.Threading; |
|||
|
|||
/// <summary>
|
|||
/// A timer that is integrated into the Dispatcher queues, and will
|
|||
/// be processed after a given amount of time at a specified priority.
|
|||
/// </summary>
|
|||
public partial class DispatcherTimer |
|||
{ |
|||
/// <summary>
|
|||
/// A timer that uses a <see cref="Dispatcher"/> to fire at a specified interval.
|
|||
/// Creates a timer that uses theUI thread's Dispatcher2 to
|
|||
/// process the timer event at background priority.
|
|||
/// </summary>
|
|||
public class DispatcherTimer |
|||
public DispatcherTimer() : this(DispatcherPriority.Background) |
|||
{ |
|||
private IDisposable? _timer; |
|||
} |
|||
|
|||
private readonly DispatcherPriority _priority; |
|||
/// <summary>
|
|||
/// Creates a timer that uses the UI thread's Dispatcher2 to
|
|||
/// process the timer event at the specified priority.
|
|||
/// </summary>
|
|||
/// <param name="priority">
|
|||
/// The priority to process the timer at.
|
|||
/// </param>
|
|||
public DispatcherTimer(DispatcherPriority priority) : this(Threading.Dispatcher.UIThread, priority, |
|||
TimeSpan.FromMilliseconds(0)) |
|||
{ |
|||
} |
|||
|
|||
private TimeSpan _interval; |
|||
/// <summary>
|
|||
/// Creates a timer that uses the specified Dispatcher2 to
|
|||
/// process the timer event at the specified priority.
|
|||
/// </summary>
|
|||
/// <param name="priority">
|
|||
/// The priority to process the timer at.
|
|||
/// </param>
|
|||
/// <param name="dispatcher">
|
|||
/// The dispatcher to use to process the timer.
|
|||
/// </param>
|
|||
internal DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher) : this(dispatcher, priority, |
|||
TimeSpan.FromMilliseconds(0)) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
|
|||
/// </summary>
|
|||
public DispatcherTimer() : this(DispatcherPriority.Background) |
|||
/// <summary>
|
|||
/// Creates a timer that uses the UI thread's Dispatcher2 to
|
|||
/// process the timer event at the specified priority after the specified timeout.
|
|||
/// </summary>
|
|||
/// <param name="interval">
|
|||
/// The interval to tick the timer after.
|
|||
/// </param>
|
|||
/// <param name="priority">
|
|||
/// The priority to process the timer at.
|
|||
/// </param>
|
|||
/// <param name="callback">
|
|||
/// The callback to call when the timer ticks.
|
|||
/// </param>
|
|||
public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback) |
|||
: this(Threading.Dispatcher.UIThread, priority, interval) |
|||
{ |
|||
if (callback == null) |
|||
{ |
|||
throw new ArgumentNullException("callback"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
|
|||
/// </summary>
|
|||
/// <param name="priority">The priority to use.</param>
|
|||
public DispatcherTimer(DispatcherPriority priority) |
|||
{ |
|||
_priority = priority; |
|||
} |
|||
Tick += callback; |
|||
Start(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
|
|||
/// </summary>
|
|||
/// <param name="interval">The interval at which to tick.</param>
|
|||
/// <param name="priority">The priority to use.</param>
|
|||
/// <param name="callback">The event to call when the timer ticks.</param>
|
|||
public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback) : this(priority) |
|||
{ |
|||
_priority = priority; |
|||
Interval = interval; |
|||
Tick += callback; |
|||
} |
|||
/// <summary>
|
|||
/// Gets the dispatcher this timer is associated with.
|
|||
/// </summary>
|
|||
public Dispatcher Dispatcher |
|||
{ |
|||
get { return _dispatcher; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Finalizes an instance of the <see cref="DispatcherTimer"/> class.
|
|||
/// </summary>
|
|||
~DispatcherTimer() |
|||
/// <summary>
|
|||
/// Gets or sets whether the timer is running.
|
|||
/// </summary>
|
|||
public bool IsEnabled |
|||
{ |
|||
get { return _isEnabled; } |
|||
|
|||
set |
|||
{ |
|||
if (_timer != null) |
|||
lock (_instanceLock) |
|||
{ |
|||
Stop(); |
|||
if (!value && _isEnabled) |
|||
{ |
|||
Stop(); |
|||
} |
|||
else if (value && !_isEnabled) |
|||
{ |
|||
Start(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raised when the timer ticks.
|
|||
/// </summary>
|
|||
public event EventHandler? Tick; |
|||
/// <summary>
|
|||
/// Gets or sets the time between timer ticks.
|
|||
/// </summary>
|
|||
public TimeSpan Interval |
|||
{ |
|||
get { return _interval; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the interval at which the timer ticks.
|
|||
/// </summary>
|
|||
public TimeSpan Interval |
|||
set |
|||
{ |
|||
get |
|||
bool updateOSTimer = false; |
|||
|
|||
if (value.TotalMilliseconds < 0) |
|||
throw new ArgumentOutOfRangeException("value", |
|||
"TimeSpan period must be greater than or equal to zero."); |
|||
|
|||
if (value.TotalMilliseconds > Int32.MaxValue) |
|||
throw new ArgumentOutOfRangeException("value", |
|||
"TimeSpan period must be less than or equal to Int32.MaxValue."); |
|||
|
|||
lock (_instanceLock) |
|||
{ |
|||
return _interval; |
|||
_interval = value; |
|||
|
|||
if (_isEnabled) |
|||
{ |
|||
DueTimeInMs = _dispatcher.Now + (long)_interval.TotalMilliseconds; |
|||
updateOSTimer = true; |
|||
} |
|||
} |
|||
|
|||
set |
|||
if (updateOSTimer) |
|||
{ |
|||
bool enabled = IsEnabled; |
|||
Stop(); |
|||
_interval = value; |
|||
IsEnabled = enabled; |
|||
_dispatcher.RescheduleTimers(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the timer is running.
|
|||
/// </summary>
|
|||
public bool IsEnabled |
|||
/// <summary>
|
|||
/// Starts the timer.
|
|||
/// </summary>
|
|||
public void Start() |
|||
{ |
|||
lock (_instanceLock) |
|||
{ |
|||
get |
|||
if (!_isEnabled) |
|||
{ |
|||
return _timer != null; |
|||
_isEnabled = true; |
|||
|
|||
Restart(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
set |
|||
/// <summary>
|
|||
/// Stops the timer.
|
|||
/// </summary>
|
|||
public void Stop() |
|||
{ |
|||
bool updateOSTimer = false; |
|||
|
|||
lock (_instanceLock) |
|||
{ |
|||
if (_isEnabled) |
|||
{ |
|||
if (IsEnabled != value) |
|||
_isEnabled = false; |
|||
updateOSTimer = true; |
|||
|
|||
// If the operation is in the queue, abort it.
|
|||
if (_operation != null) |
|||
{ |
|||
if (value) |
|||
{ |
|||
Start(); |
|||
} |
|||
else |
|||
{ |
|||
Stop(); |
|||
} |
|||
_operation.Abort(); |
|||
_operation = null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets user-defined data associated with the timer.
|
|||
/// </summary>
|
|||
public object? Tag |
|||
if (updateOSTimer) |
|||
{ |
|||
get; |
|||
set; |
|||
_dispatcher.RemoveTimer(this); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Starts a new timer.
|
|||
/// </summary>
|
|||
/// <param name="action">
|
|||
/// The method to call on timer tick. If the method returns false, the timer will stop.
|
|||
/// </param>
|
|||
/// <param name="interval">The interval at which to tick.</param>
|
|||
/// <param name="priority">The priority to use.</param>
|
|||
/// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
|
|||
public static IDisposable Run(Func<bool> action, TimeSpan interval, DispatcherPriority priority = default) |
|||
{ |
|||
var timer = new DispatcherTimer(priority) { Interval = interval }; |
|||
|
|||
/// <summary>
|
|||
/// Starts a new timer.
|
|||
/// </summary>
|
|||
/// <param name="action">
|
|||
/// The method to call on timer tick. If the method returns false, the timer will stop.
|
|||
/// </param>
|
|||
/// <param name="interval">The interval at which to tick.</param>
|
|||
/// <param name="priority">The priority to use.</param>
|
|||
/// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
|
|||
public static IDisposable Run(Func<bool> action, TimeSpan interval, DispatcherPriority priority = default) |
|||
timer.Tick += (s, e) => |
|||
{ |
|||
var timer = new DispatcherTimer(priority) { Interval = interval }; |
|||
|
|||
timer.Tick += (s, e) => |
|||
if (!action()) |
|||
{ |
|||
if (!action()) |
|||
{ |
|||
timer.Stop(); |
|||
} |
|||
}; |
|||
timer.Stop(); |
|||
} |
|||
}; |
|||
|
|||
timer.Start(); |
|||
timer.Start(); |
|||
|
|||
return Disposable.Create(() => timer.Stop()); |
|||
} |
|||
return Disposable.Create(() => timer.Stop()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Runs a method once, after the specified interval.
|
|||
/// </summary>
|
|||
/// <param name="action">
|
|||
/// The method to call after the interval has elapsed.
|
|||
/// </param>
|
|||
/// <param name="interval">The interval after which to call the method.</param>
|
|||
/// <param name="priority">The priority to use.</param>
|
|||
/// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
|
|||
public static IDisposable RunOnce( |
|||
Action action, |
|||
TimeSpan interval, |
|||
DispatcherPriority priority = default) |
|||
{ |
|||
interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); |
|||
|
|||
var timer = new DispatcherTimer(priority) { Interval = interval }; |
|||
|
|||
/// <summary>
|
|||
/// Runs a method once, after the specified interval.
|
|||
/// </summary>
|
|||
/// <param name="action">
|
|||
/// The method to call after the interval has elapsed.
|
|||
/// </param>
|
|||
/// <param name="interval">The interval after which to call the method.</param>
|
|||
/// <param name="priority">The priority to use.</param>
|
|||
/// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
|
|||
public static IDisposable RunOnce( |
|||
Action action, |
|||
TimeSpan interval, |
|||
DispatcherPriority priority = default) |
|||
timer.Tick += (s, e) => |
|||
{ |
|||
interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); |
|||
|
|||
var timer = new DispatcherTimer(priority) { Interval = interval }; |
|||
action(); |
|||
timer.Stop(); |
|||
}; |
|||
|
|||
timer.Start(); |
|||
|
|||
return Disposable.Create(() => timer.Stop()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Occurs when the specified timer interval has elapsed and the
|
|||
/// timer is enabled.
|
|||
/// </summary>
|
|||
public event EventHandler? Tick; |
|||
|
|||
/// <summary>
|
|||
/// Any data that the caller wants to pass along with the timer.
|
|||
/// </summary>
|
|||
public object? Tag { get; set; } |
|||
|
|||
timer.Tick += (s, e) => |
|||
{ |
|||
action(); |
|||
timer.Stop(); |
|||
}; |
|||
|
|||
timer.Start(); |
|||
internal DispatcherTimer(Dispatcher dispatcher, DispatcherPriority priority, TimeSpan interval) |
|||
{ |
|||
if (dispatcher == null) |
|||
{ |
|||
throw new ArgumentNullException("dispatcher"); |
|||
} |
|||
|
|||
return Disposable.Create(() => timer.Stop()); |
|||
DispatcherPriority.Validate(priority, "priority"); |
|||
if (priority == DispatcherPriority.Inactive) |
|||
{ |
|||
throw new ArgumentException("Specified priority is not valid.", "priority"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Starts the timer.
|
|||
/// </summary>
|
|||
public void Start() |
|||
if (interval.TotalMilliseconds < 0) |
|||
throw new ArgumentOutOfRangeException("interval", "TimeSpan period must be greater than or equal to zero."); |
|||
|
|||
if (interval.TotalMilliseconds > Int32.MaxValue) |
|||
throw new ArgumentOutOfRangeException("interval", |
|||
"TimeSpan period must be less than or equal to Int32.MaxValue."); |
|||
|
|||
|
|||
_dispatcher = dispatcher; |
|||
_priority = priority; |
|||
_interval = interval; |
|||
} |
|||
|
|||
private void Restart() |
|||
{ |
|||
lock (_instanceLock) |
|||
{ |
|||
if (!IsEnabled) |
|||
if (_operation != null) |
|||
{ |
|||
var threading = AvaloniaLocator.Current.GetRequiredService<IPlatformThreadingInterface>(); |
|||
_timer = threading.StartTimer(_priority, Interval, InternalTick); |
|||
// Timer has already been restarted, e.g. Start was called form the Tick handler.
|
|||
return; |
|||
} |
|||
|
|||
// BeginInvoke a new operation.
|
|||
_operation = _dispatcher.InvokeAsync(FireTick, DispatcherPriority.Inactive); |
|||
|
|||
DueTimeInMs = _dispatcher.Now + (long)_interval.TotalMilliseconds; |
|||
|
|||
if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess()) |
|||
{ |
|||
// shortcut - just promote the item now
|
|||
Promote(); |
|||
} |
|||
else |
|||
{ |
|||
_dispatcher.AddTimer(this); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stops the timer.
|
|||
/// </summary>
|
|||
public void Stop() |
|||
internal void Promote() // called from Dispatcher
|
|||
{ |
|||
lock (_instanceLock) |
|||
{ |
|||
if (IsEnabled) |
|||
// Simply promote the operation to it's desired priority.
|
|||
if (_operation != null) |
|||
{ |
|||
_timer!.Dispose(); |
|||
_timer = null; |
|||
_operation.Priority = _priority; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void FireTick() |
|||
{ |
|||
// The operation has been invoked, so forget about it.
|
|||
_operation = null; |
|||
|
|||
// The dispatcher thread is calling us because item's priority
|
|||
// was changed from inactive to something else.
|
|||
if (Tick != null) |
|||
{ |
|||
Tick(this, EventArgs.Empty); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raises the <see cref="Tick"/> event on the dispatcher thread.
|
|||
/// </summary>
|
|||
private void InternalTick() |
|||
// If we are still enabled, start the timer again.
|
|||
if (_isEnabled) |
|||
{ |
|||
Dispatcher.UIThread.EnsurePriority(_priority); |
|||
Tick?.Invoke(this, EventArgs.Empty); |
|||
Restart(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// This is the object we use to synchronize access.
|
|||
private object _instanceLock = new object(); |
|||
|
|||
// Note: We cannot BE a dispatcher-affinity object because we can be
|
|||
// created by a worker thread. We are still associated with a
|
|||
// dispatcher (where we post the item) but we can be accessed
|
|||
// by any thread.
|
|||
private Dispatcher _dispatcher; |
|||
|
|||
private DispatcherPriority _priority; // NOTE: should be Priority
|
|||
private TimeSpan _interval; |
|||
private DispatcherOperation? _operation; |
|||
private bool _isEnabled; |
|||
|
|||
// used by Dispatcher
|
|||
internal long DueTimeInMs { get; private set; } |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Threading; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Threading; |
|||
|
|||
[Unstable] |
|||
public interface IDispatcherImpl |
|||
{ |
|||
bool CurrentThreadIsLoopThread { get; } |
|||
|
|||
// Asynchronously triggers Signaled callback
|
|||
void Signal(); |
|||
event Action Signaled; |
|||
event Action Timer; |
|||
long Now { get; } |
|||
void UpdateTimer(long? dueTimeInMs); |
|||
} |
|||
|
|||
[Unstable] |
|||
public interface IDispatcherImplWithPendingInput : IDispatcherImpl |
|||
{ |
|||
// Checks if dispatcher implementation can
|
|||
bool CanQueryPendingInput { get; } |
|||
// Checks if there is pending user input
|
|||
bool HasPendingInput { get; } |
|||
} |
|||
|
|||
[Unstable] |
|||
public interface IDispatcherImplWithExplicitBackgroundProcessing : IDispatcherImpl |
|||
{ |
|||
event Action ReadyForBackgroundProcessing; |
|||
void RequestBackgroundProcessing(); |
|||
} |
|||
|
|||
[Unstable] |
|||
public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput |
|||
{ |
|||
// Runs the event loop
|
|||
void RunLoop(CancellationToken token); |
|||
} |
|||
|
|||
internal class LegacyDispatcherImpl : IDispatcherImpl |
|||
{ |
|||
private readonly IPlatformThreadingInterface _platformThreading; |
|||
private IDisposable? _timer; |
|||
private Stopwatch _clock = Stopwatch.StartNew(); |
|||
|
|||
public LegacyDispatcherImpl(IPlatformThreadingInterface platformThreading) |
|||
{ |
|||
_platformThreading = platformThreading; |
|||
_platformThreading.Signaled += delegate { Signaled?.Invoke(); }; |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread => _platformThreading.CurrentThreadIsLoopThread; |
|||
public void Signal() => _platformThreading.Signal(DispatcherPriority.Send); |
|||
|
|||
public event Action? Signaled; |
|||
public event Action? Timer; |
|||
public long Now => _clock.ElapsedMilliseconds; |
|||
public void UpdateTimer(long? dueTimeInMs) |
|||
{ |
|||
_timer?.Dispose(); |
|||
_timer = null; |
|||
|
|||
if (dueTimeInMs.HasValue) |
|||
{ |
|||
var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds); |
|||
_timer = _platformThreading.StartTimer(DispatcherPriority.Send, |
|||
TimeSpan.FromMilliseconds(interval), |
|||
OnTick); |
|||
} |
|||
} |
|||
|
|||
private void OnTick() |
|||
{ |
|||
_timer?.Dispose(); |
|||
_timer = null; |
|||
Timer?.Invoke(); |
|||
} |
|||
} |
|||
|
|||
class NullDispatcherImpl : IDispatcherImpl |
|||
{ |
|||
public bool CurrentThreadIsLoopThread => true; |
|||
|
|||
public void Signal() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public event Action? Signaled; |
|||
public event Action? Timer; |
|||
|
|||
public long Now => 0; |
|||
|
|||
public void UpdateTimer(long? dueTimeInMs) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -1,300 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Threading |
|||
{ |
|||
/// <summary>
|
|||
/// A main loop in a <see cref="Dispatcher"/>.
|
|||
/// </summary>
|
|||
internal class JobRunner |
|||
{ |
|||
private IPlatformThreadingInterface? _platform; |
|||
|
|||
private readonly Queue<IJob>[] _queues = Enumerable.Range(0, (int)DispatcherPriority.MaxValue + 1) |
|||
.Select(_ => new Queue<IJob>()).ToArray(); |
|||
|
|||
public JobRunner(IPlatformThreadingInterface? platform) |
|||
{ |
|||
_platform = platform; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Runs continuations pushed on the loop.
|
|||
/// </summary>
|
|||
/// <param name="priority">Priority to execute jobs for. Pass null if platform doesn't have internal priority system</param>
|
|||
public void RunJobs(DispatcherPriority? priority) |
|||
{ |
|||
var minimumPriority = priority ?? DispatcherPriority.MinValue; |
|||
while (true) |
|||
{ |
|||
var job = GetNextJob(minimumPriority); |
|||
if (job == null) |
|||
return; |
|||
|
|||
job.Run(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invokes a method on the main loop.
|
|||
/// </summary>
|
|||
/// <param name="action">The method.</param>
|
|||
/// <param name="priority">The priority with which to invoke the method.</param>
|
|||
/// <returns>A task that can be used to track the method's execution.</returns>
|
|||
public Task InvokeAsync(Action action, DispatcherPriority priority) |
|||
{ |
|||
var job = new Job(action, priority, false); |
|||
AddJob(job); |
|||
return job.Task!; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invokes a method on the main loop.
|
|||
/// </summary>
|
|||
/// <param name="function">The method.</param>
|
|||
/// <param name="priority">The priority with which to invoke the method.</param>
|
|||
/// <returns>A task that can be used to track the method's execution.</returns>
|
|||
public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority) |
|||
{ |
|||
var job = new JobWithResult<TResult>(function, priority); |
|||
AddJob(job); |
|||
return job.Task; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Post action that will be invoked on main thread
|
|||
/// </summary>
|
|||
/// <param name="action">The method.</param>
|
|||
///
|
|||
/// <param name="priority">The priority with which to invoke the method.</param>
|
|||
internal void Post(Action action, DispatcherPriority priority) |
|||
{ |
|||
AddJob(new Job(action, priority, true)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Post action that will be invoked on main thread
|
|||
/// </summary>
|
|||
/// <param name="action">The method to call.</param>
|
|||
/// <param name="parameter">The parameter of method to call.</param>
|
|||
/// <param name="priority">The priority with which to invoke the method.</param>
|
|||
internal void Post(SendOrPostCallback action, object? parameter, DispatcherPriority priority) |
|||
{ |
|||
AddJob(new JobWithArg(action, parameter, priority, true)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Allows unit tests to change the platform threading interface.
|
|||
/// </summary>
|
|||
internal void UpdateServices() |
|||
{ |
|||
_platform = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>(); |
|||
} |
|||
|
|||
private void AddJob(IJob job) |
|||
{ |
|||
bool needWake; |
|||
var queue = _queues[(int)job.Priority]; |
|||
lock (queue) |
|||
{ |
|||
needWake = queue.Count == 0; |
|||
queue.Enqueue(job); |
|||
} |
|||
if (needWake) |
|||
_platform?.Signal(job.Priority); |
|||
} |
|||
|
|||
private IJob? GetNextJob(DispatcherPriority minimumPriority) |
|||
{ |
|||
for (int c = (int)DispatcherPriority.MaxValue; c >= (int)minimumPriority; c--) |
|||
{ |
|||
var q = _queues[c]; |
|||
lock (q) |
|||
{ |
|||
if (q.Count > 0) |
|||
return q.Dequeue(); |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public bool HasJobsWithPriority(DispatcherPriority minimumPriority) |
|||
{ |
|||
for (int c = (int)minimumPriority; c < (int)DispatcherPriority.MaxValue; c++) |
|||
{ |
|||
var q = _queues[c]; |
|||
lock (q) |
|||
{ |
|||
if (q.Count > 0) |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private interface IJob |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the job priority.
|
|||
/// </summary>
|
|||
DispatcherPriority Priority { get; } |
|||
|
|||
/// <summary>
|
|||
/// Runs the job.
|
|||
/// </summary>
|
|||
void Run(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A job to run.
|
|||
/// </summary>
|
|||
private sealed class Job : IJob |
|||
{ |
|||
/// <summary>
|
|||
/// The method to call.
|
|||
/// </summary>
|
|||
private readonly Action _action; |
|||
/// <summary>
|
|||
/// The task completion source.
|
|||
/// </summary>
|
|||
private readonly TaskCompletionSource<object?>? _taskCompletionSource; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Job"/> class.
|
|||
/// </summary>
|
|||
/// <param name="action">The method to call.</param>
|
|||
/// <param name="priority">The job priority.</param>
|
|||
/// <param name="throwOnUiThread">Do not wrap exception in TaskCompletionSource</param>
|
|||
public Job(Action action, DispatcherPriority priority, bool throwOnUiThread) |
|||
{ |
|||
_action = action; |
|||
Priority = priority; |
|||
_taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource<object?>(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public DispatcherPriority Priority { get; } |
|||
|
|||
/// <summary>
|
|||
/// The task.
|
|||
/// </summary>
|
|||
public Task? Task => _taskCompletionSource?.Task; |
|||
|
|||
/// <inheritdoc/>
|
|||
void IJob.Run() |
|||
{ |
|||
if (_taskCompletionSource == null) |
|||
{ |
|||
_action(); |
|||
return; |
|||
} |
|||
try |
|||
{ |
|||
_action(); |
|||
_taskCompletionSource.SetResult(null); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
_taskCompletionSource.SetException(e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A typed job to run.
|
|||
/// </summary>
|
|||
private sealed class JobWithArg : IJob |
|||
{ |
|||
private readonly SendOrPostCallback _action; |
|||
private readonly object? _parameter; |
|||
private readonly TaskCompletionSource<bool>? _taskCompletionSource; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Job"/> class.
|
|||
/// </summary>
|
|||
/// <param name="action">The method to call.</param>
|
|||
/// <param name="parameter">The parameter of method to call.</param>
|
|||
/// <param name="priority">The job priority.</param>
|
|||
/// <param name="throwOnUiThread">Do not wrap exception in TaskCompletionSource</param>
|
|||
|
|||
public JobWithArg(SendOrPostCallback action, object? parameter, DispatcherPriority priority, bool throwOnUiThread) |
|||
{ |
|||
_action = action; |
|||
_parameter = parameter; |
|||
Priority = priority; |
|||
_taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource<bool>(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public DispatcherPriority Priority { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
void IJob.Run() |
|||
{ |
|||
if (_taskCompletionSource == null) |
|||
{ |
|||
_action(_parameter); |
|||
return; |
|||
} |
|||
try |
|||
{ |
|||
_action(_parameter); |
|||
_taskCompletionSource.SetResult(default); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
_taskCompletionSource.SetException(e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A job to run thath return value.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResult">Type of job result</typeparam>
|
|||
private sealed class JobWithResult<TResult> : IJob |
|||
{ |
|||
private readonly Func<TResult> _function; |
|||
private readonly TaskCompletionSource<TResult> _taskCompletionSource; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Job"/> class.
|
|||
/// </summary>
|
|||
/// <param name="function">The method to call.</param>
|
|||
/// <param name="priority">The job priority.</param>
|
|||
public JobWithResult(Func<TResult> function, DispatcherPriority priority) |
|||
{ |
|||
_function = function; |
|||
Priority = priority; |
|||
_taskCompletionSource = new TaskCompletionSource<TResult>(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public DispatcherPriority Priority { get; } |
|||
|
|||
/// <summary>
|
|||
/// The task.
|
|||
/// </summary>
|
|||
public Task<TResult> Task => _taskCompletionSource.Task; |
|||
|
|||
/// <inheritdoc/>
|
|||
void IJob.Run() |
|||
{ |
|||
try |
|||
{ |
|||
var result = _function(); |
|||
_taskCompletionSource.SetResult(result); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
_taskCompletionSource.SetException(e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the position of a color's alpha component relative to all other components.
|
|||
/// </summary>
|
|||
public enum AlphaComponentPosition |
|||
{ |
|||
/// <summary>
|
|||
/// The alpha component occurs before all other components.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// For example, this may indicate the #AARRGGBB or ARGB format which
|
|||
/// is the default format for XAML itself and the Color struct.
|
|||
/// </remarks>
|
|||
Leading, |
|||
|
|||
/// <summary>
|
|||
/// The alpha component occurs after all other components.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// For example, this may indicate the #RRGGBBAA or RGBA format which
|
|||
/// is the default format for CSS.
|
|||
/// </remarks>
|
|||
Trailing, |
|||
} |
|||
} |
|||
@ -1,92 +0,0 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Controls.Platform |
|||
{ |
|||
[Unstable] |
|||
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface |
|||
{ |
|||
public InternalPlatformThreadingInterface() |
|||
{ |
|||
TlsCurrentThreadIsLoopThread = true; |
|||
} |
|||
|
|||
private readonly AutoResetEvent _signaled = new AutoResetEvent(false); |
|||
|
|||
|
|||
public void RunLoop(CancellationToken cancellationToken) |
|||
{ |
|||
var handles = new[] { _signaled, cancellationToken.WaitHandle }; |
|||
|
|||
while (!cancellationToken.IsCancellationRequested) |
|||
{ |
|||
Signaled?.Invoke(null); |
|||
WaitHandle.WaitAny(handles); |
|||
} |
|||
} |
|||
|
|||
|
|||
class TimerImpl : IDisposable |
|||
{ |
|||
private readonly DispatcherPriority _priority; |
|||
private readonly TimeSpan _interval; |
|||
private readonly Action _tick; |
|||
private Timer? _timer; |
|||
private GCHandle _handle; |
|||
|
|||
public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick) |
|||
{ |
|||
_priority = priority; |
|||
_interval = interval; |
|||
_tick = tick; |
|||
_timer = new Timer(OnTimer, null, interval, Timeout.InfiniteTimeSpan); |
|||
_handle = GCHandle.Alloc(_timer); |
|||
} |
|||
|
|||
private void OnTimer(object? state) |
|||
{ |
|||
if (_timer == null) |
|||
return; |
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
|
|||
if (_timer == null) |
|||
return; |
|||
_tick(); |
|||
_timer?.Change(_interval, Timeout.InfiniteTimeSpan); |
|||
}); |
|||
} |
|||
|
|||
|
|||
public void Dispose() |
|||
{ |
|||
_handle.Free(); |
|||
_timer?.Dispose(); |
|||
_timer = null; |
|||
} |
|||
} |
|||
|
|||
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) |
|||
{ |
|||
return new TimerImpl(priority, interval, tick); |
|||
} |
|||
|
|||
public void Signal(DispatcherPriority prio) |
|||
{ |
|||
_signaled.Set(); |
|||
} |
|||
|
|||
[ThreadStatic] private static bool TlsCurrentThreadIsLoopThread; |
|||
|
|||
public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread; |
|||
public event Action<DispatcherPriority?>? Signaled; |
|||
#pragma warning disable CS0067
|
|||
public event Action<TimeSpan>? Tick; |
|||
#pragma warning restore CS0067
|
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Threading; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Controls.Platform; |
|||
|
|||
[Unstable] |
|||
public class ManagedDispatcherImpl : IControlledDispatcherImpl |
|||
{ |
|||
private readonly IManagedDispatcherInputProvider? _inputProvider; |
|||
private readonly AutoResetEvent _wakeup = new(false); |
|||
private bool _signaled; |
|||
private readonly object _lock = new(); |
|||
private readonly Stopwatch _clock = Stopwatch.StartNew(); |
|||
private TimeSpan? _nextTimer; |
|||
private readonly Thread _loopThread = Thread.CurrentThread; |
|||
|
|||
public interface IManagedDispatcherInputProvider |
|||
{ |
|||
bool HasInput { get; } |
|||
void DispatchNextInputEvent(); |
|||
} |
|||
|
|||
public ManagedDispatcherImpl(IManagedDispatcherInputProvider? inputProvider) |
|||
{ |
|||
_inputProvider = inputProvider; |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread => _loopThread == Thread.CurrentThread; |
|||
public void Signal() |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
_signaled = true; |
|||
_wakeup.Set(); |
|||
} |
|||
} |
|||
|
|||
public event Action? Signaled; |
|||
public event Action? Timer; |
|||
public long Now => _clock.ElapsedMilliseconds; |
|||
public void UpdateTimer(long? dueTimeInMs) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
_nextTimer = dueTimeInMs == null |
|||
? null |
|||
: TimeSpan.FromMilliseconds(dueTimeInMs.Value); |
|||
if (!CurrentThreadIsLoopThread) |
|||
_wakeup.Set(); |
|||
} |
|||
} |
|||
|
|||
public bool CanQueryPendingInput => _inputProvider != null; |
|||
public bool HasPendingInput => _inputProvider?.HasInput ?? false; |
|||
|
|||
public void RunLoop(CancellationToken token) |
|||
{ |
|||
while (!token.IsCancellationRequested) |
|||
{ |
|||
bool signaled; |
|||
lock (_lock) |
|||
{ |
|||
signaled = _signaled; |
|||
_signaled = false; |
|||
} |
|||
|
|||
if (signaled) |
|||
{ |
|||
Signaled?.Invoke(); |
|||
continue; |
|||
} |
|||
|
|||
bool fireTimer = false; |
|||
lock (_lock) |
|||
{ |
|||
if (_nextTimer < _clock.Elapsed) |
|||
{ |
|||
fireTimer = true; |
|||
_nextTimer = null; |
|||
} |
|||
} |
|||
|
|||
if (fireTimer) |
|||
{ |
|||
Timer?.Invoke(); |
|||
continue; |
|||
} |
|||
|
|||
if (_inputProvider?.HasInput == true) |
|||
{ |
|||
_inputProvider.DispatchNextInputEvent(); |
|||
continue; |
|||
} |
|||
|
|||
if (_nextTimer != null) |
|||
{ |
|||
var waitFor = _clock.Elapsed - _nextTimer.Value; |
|||
if (waitFor.TotalMilliseconds < 1) |
|||
continue; |
|||
_wakeup.WaitOne(waitFor); |
|||
} |
|||
else |
|||
_wakeup.WaitOne(); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,86 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Reactive; |
|||
using System.Threading; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Headless |
|||
{ |
|||
class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface |
|||
{ |
|||
public HeadlessPlatformThreadingInterface() |
|||
{ |
|||
_thread = Thread.CurrentThread; |
|||
} |
|||
|
|||
private AutoResetEvent _event = new AutoResetEvent(false); |
|||
private Thread _thread; |
|||
private object _lock = new object(); |
|||
private DispatcherPriority? _signaledPriority; |
|||
|
|||
public void RunLoop(CancellationToken cancellationToken) |
|||
{ |
|||
while (!cancellationToken.IsCancellationRequested) |
|||
{ |
|||
DispatcherPriority? signaled = null; |
|||
lock (_lock) |
|||
{ |
|||
signaled = _signaledPriority; |
|||
_signaledPriority = null; |
|||
} |
|||
if(signaled.HasValue) |
|||
Signaled?.Invoke(signaled); |
|||
WaitHandle.WaitAny(new[] {cancellationToken.WaitHandle, _event}, TimeSpan.FromMilliseconds(20)); |
|||
} |
|||
} |
|||
|
|||
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) |
|||
{ |
|||
if (interval.TotalMilliseconds < 10) |
|||
interval = TimeSpan.FromMilliseconds(10); |
|||
|
|||
var stopped = false; |
|||
Timer timer = null; |
|||
timer = new Timer(_ => |
|||
{ |
|||
if (stopped) |
|||
return; |
|||
|
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
try |
|||
{ |
|||
tick(); |
|||
} |
|||
finally |
|||
{ |
|||
if (!stopped) |
|||
timer.Change(interval, Timeout.InfiniteTimeSpan); |
|||
} |
|||
}); |
|||
}, |
|||
null, interval, Timeout.InfiniteTimeSpan); |
|||
|
|||
return Disposable.Create(() => |
|||
{ |
|||
stopped = true; |
|||
timer.Dispose(); |
|||
}); |
|||
} |
|||
|
|||
public void Signal(DispatcherPriority priority) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
if (_signaledPriority == null || _signaledPriority.Value > priority) |
|||
{ |
|||
_signaledPriority = priority; |
|||
} |
|||
_event.Set(); |
|||
} |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread; |
|||
public event Action<DispatcherPriority?> Signaled; |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Runtime.ExceptionServices; |
|||
using System.Threading; |
|||
using Avalonia.Native.Interop; |
|||
using Avalonia.Threading; |
|||
using MicroCom.Runtime; |
|||
|
|||
namespace Avalonia.Native; |
|||
|
|||
internal class DispatcherImpl : IControlledDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing |
|||
{ |
|||
private readonly IAvnPlatformThreadingInterface _native; |
|||
private Thread? _loopThread; |
|||
private Stopwatch _clock = Stopwatch.StartNew(); |
|||
private Stack<RunLoopFrame> _managedFrames = new(); |
|||
|
|||
public DispatcherImpl(IAvnPlatformThreadingInterface native) |
|||
{ |
|||
_native = native; |
|||
using var events = new Events(this); |
|||
_native.SetEvents(events); |
|||
} |
|||
|
|||
public event Action Signaled; |
|||
public event Action Timer; |
|||
public event Action ReadyForBackgroundProcessing; |
|||
|
|||
private class Events : NativeCallbackBase, IAvnPlatformThreadingInterfaceEvents |
|||
{ |
|||
private readonly DispatcherImpl _parent; |
|||
|
|||
public Events(DispatcherImpl parent) |
|||
{ |
|||
_parent = parent; |
|||
} |
|||
public void Signaled() => _parent.Signaled?.Invoke(); |
|||
|
|||
public void Timer() => _parent.Timer?.Invoke(); |
|||
|
|||
public void ReadyForBackgroundProcessing() => _parent.ReadyForBackgroundProcessing?.Invoke(); |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread |
|||
{ |
|||
get |
|||
{ |
|||
if (_loopThread != null) |
|||
return Thread.CurrentThread == _loopThread; |
|||
if (_native.CurrentThreadIsLoopThread == 0) |
|||
return false; |
|||
_loopThread = Thread.CurrentThread; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
public void Signal() => _native.Signal(); |
|||
|
|||
public void UpdateTimer(long? dueTimeInMs) |
|||
{ |
|||
var ms = dueTimeInMs == null ? -1 : (int)Math.Min(int.MaxValue - 10, Math.Max(1, dueTimeInMs.Value - Now)); |
|||
_native.UpdateTimer(ms); |
|||
} |
|||
|
|||
public bool CanQueryPendingInput => false; |
|||
public bool HasPendingInput => false; |
|||
|
|||
class RunLoopFrame : IDisposable |
|||
{ |
|||
public ExceptionDispatchInfo? Exception; |
|||
public CancellationTokenSource CancellationTokenSource = new(); |
|||
|
|||
public RunLoopFrame(CancellationToken token) |
|||
{ |
|||
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); |
|||
} |
|||
|
|||
public void Dispose() => CancellationTokenSource.Dispose(); |
|||
} |
|||
|
|||
public void RunLoop(CancellationToken token) |
|||
{ |
|||
if (token.IsCancellationRequested) |
|||
return; |
|||
object l = new(); |
|||
var exited = false; |
|||
|
|||
using var frame = new RunLoopFrame(token); |
|||
|
|||
using var cancel = _native.CreateLoopCancellation(); |
|||
frame.CancellationTokenSource.Token.Register(() => |
|||
{ |
|||
lock (l) |
|||
// ReSharper disable once AccessToModifiedClosure
|
|||
// ReSharper disable once AccessToDisposedClosure
|
|||
if (!exited) |
|||
cancel.Cancel(); |
|||
}); |
|||
|
|||
try |
|||
{ |
|||
_managedFrames.Push(frame); |
|||
_native.RunLoop(cancel); |
|||
} |
|||
finally |
|||
{ |
|||
lock (l) |
|||
exited = true; |
|||
_managedFrames.Pop(); |
|||
if (frame.Exception != null) |
|||
frame.Exception.Throw(); |
|||
} |
|||
} |
|||
|
|||
public long Now => _clock.ElapsedMilliseconds; |
|||
|
|||
public void PropagateCallbackException(ExceptionDispatchInfo capture) |
|||
{ |
|||
if (_managedFrames.Count == 0) |
|||
{ |
|||
Debug.Assert(false, "We should never get here"); |
|||
return; |
|||
} |
|||
|
|||
var frame = _managedFrames.Peek(); |
|||
frame.Exception = capture; |
|||
frame.CancellationTokenSource.Cancel(); |
|||
} |
|||
public void RequestBackgroundProcessing() => _native.RequestBackgroundProcessing(); |
|||
} |
|||
@ -1,115 +0,0 @@ |
|||
using System; |
|||
using System.Runtime.ExceptionServices; |
|||
using System.Threading; |
|||
using Avalonia.Native.Interop; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Native |
|||
{ |
|||
internal class PlatformThreadingInterface : IPlatformThreadingInterface |
|||
{ |
|||
class TimerCallback : NativeCallbackBase, IAvnActionCallback |
|||
{ |
|||
readonly Action _tick; |
|||
|
|||
public TimerCallback(Action tick) |
|||
{ |
|||
_tick = tick; |
|||
} |
|||
|
|||
public void Run() |
|||
{ |
|||
_tick(); |
|||
} |
|||
} |
|||
|
|||
class SignaledCallback : NativeCallbackBase, IAvnSignaledCallback |
|||
{ |
|||
readonly PlatformThreadingInterface _parent; |
|||
|
|||
public SignaledCallback(PlatformThreadingInterface parent) |
|||
{ |
|||
_parent = parent; |
|||
} |
|||
|
|||
public void Signaled(int priority, int priorityContainsMeaningfulValue) |
|||
{ |
|||
_parent.Signaled?.Invoke(priorityContainsMeaningfulValue.FromComBool() ? (DispatcherPriority?)priority : null); |
|||
} |
|||
} |
|||
|
|||
readonly IAvnPlatformThreadingInterface _native; |
|||
private ExceptionDispatchInfo _exceptionDispatchInfo; |
|||
private CancellationTokenSource _exceptionCancellationSource; |
|||
|
|||
public PlatformThreadingInterface(IAvnPlatformThreadingInterface native) |
|||
{ |
|||
_native = native; |
|||
using (var cb = new SignaledCallback(this)) |
|||
_native.SetSignaledCallback(cb); |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread => _native.CurrentThreadIsLoopThread.FromComBool(); |
|||
|
|||
public event Action<DispatcherPriority?> Signaled; |
|||
|
|||
public void RunLoop(CancellationToken cancellationToken) |
|||
{ |
|||
_exceptionDispatchInfo?.Throw(); |
|||
var l = new object(); |
|||
_exceptionCancellationSource = new CancellationTokenSource(); |
|||
|
|||
var compositeCancellation = CancellationTokenSource |
|||
.CreateLinkedTokenSource(cancellationToken, _exceptionCancellationSource.Token).Token; |
|||
|
|||
var cancellation = _native.CreateLoopCancellation(); |
|||
compositeCancellation.Register(() => |
|||
{ |
|||
lock (l) |
|||
{ |
|||
cancellation?.Cancel(); |
|||
} |
|||
}); |
|||
|
|||
try |
|||
{ |
|||
_native.RunLoop(cancellation); |
|||
} |
|||
finally |
|||
{ |
|||
lock (l) |
|||
{ |
|||
cancellation?.Dispose(); |
|||
cancellation = null; |
|||
} |
|||
} |
|||
|
|||
if (_exceptionDispatchInfo != null) |
|||
{ |
|||
_exceptionDispatchInfo.Throw(); |
|||
} |
|||
} |
|||
|
|||
public void DispatchException (ExceptionDispatchInfo exceptionInfo) |
|||
{ |
|||
_exceptionDispatchInfo = exceptionInfo; |
|||
} |
|||
|
|||
public void TerminateNativeApp() |
|||
{ |
|||
_exceptionCancellationSource?.Cancel(); |
|||
} |
|||
|
|||
public void Signal(DispatcherPriority priority) |
|||
{ |
|||
_native.Signal((int)priority); |
|||
} |
|||
|
|||
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) |
|||
{ |
|||
using (var cb = new TimerCallback(tick)) |
|||
return _native.StartTimer((int)priority, (int)interval.TotalMilliseconds, cb); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Win32.Interop; |
|||
using static Avalonia.Win32.Interop.UnmanagedMethods; |
|||
namespace Avalonia.Win32; |
|||
|
|||
internal class Win32DispatcherImpl : IControlledDispatcherImpl |
|||
{ |
|||
private readonly IntPtr _messageWindow; |
|||
private static Thread? s_uiThread; |
|||
private readonly Stopwatch _clock = Stopwatch.StartNew(); |
|||
public Win32DispatcherImpl(IntPtr messageWindow) |
|||
{ |
|||
_messageWindow = messageWindow; |
|||
s_uiThread = Thread.CurrentThread; |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread => s_uiThread == Thread.CurrentThread; |
|||
internal const int SignalW = unchecked((int)0xdeadbeaf); |
|||
internal const int SignalL = unchecked((int)0x12345678); |
|||
|
|||
public void Signal() => |
|||
// Messages from PostMessage are always processed before any user input,
|
|||
// so Win32 should call us ASAP
|
|||
PostMessage( |
|||
_messageWindow, |
|||
(int)WindowsMessage.WM_DISPATCH_WORK_ITEM, |
|||
new IntPtr(SignalW), |
|||
new IntPtr(SignalL)); |
|||
|
|||
public void DispatchWorkItem() => Signaled?.Invoke(); |
|||
|
|||
public event Action? Signaled; |
|||
public event Action? Timer; |
|||
|
|||
public void FireTimer() => Timer?.Invoke(); |
|||
|
|||
public void UpdateTimer(long? dueTimeInMs) |
|||
{ |
|||
if (dueTimeInMs == null) |
|||
{ |
|||
KillTimer(_messageWindow, (IntPtr)Win32Platform.TIMERID_DISPATCHER); |
|||
} |
|||
else |
|||
{ |
|||
var interval = (uint)Math.Min(int.MaxValue - 10, Math.Max(1, Now - dueTimeInMs.Value)); |
|||
SetTimer( |
|||
_messageWindow, |
|||
(IntPtr)Win32Platform.TIMERID_DISPATCHER, |
|||
interval, |
|||
null!); |
|||
} |
|||
} |
|||
|
|||
public bool CanQueryPendingInput => true; |
|||
|
|||
public bool HasPendingInput |
|||
{ |
|||
get |
|||
{ |
|||
// We need to know if there is any pending input in the Win32
|
|||
// queue because we want to only process Avalon "background"
|
|||
// items after Win32 input has been processed.
|
|||
//
|
|||
// Win32 provides the GetQueueStatus API -- but it has a major
|
|||
// drawback: it only counts "new" input. This means that
|
|||
// sometimes it could return false, even if there really is input
|
|||
// that needs to be processed. This results in very hard to
|
|||
// find bugs.
|
|||
//
|
|||
// Luckily, Win32 also provides the MsgWaitForMultipleObjectsEx
|
|||
// API. While more awkward to use, this API can return queue
|
|||
// status information even if the input is "old". The various
|
|||
// flags we use are:
|
|||
//
|
|||
// QS_INPUT
|
|||
// This represents any pending input - such as mouse moves, or
|
|||
// key presses. It also includes the new GenericInput messages.
|
|||
//
|
|||
// QS_EVENT
|
|||
// This is actually a private flag that represents the various
|
|||
// events that can be queued in Win32. Some of these events
|
|||
// can cause input, but Win32 doesn't include them in the
|
|||
// QS_INPUT flag. An example is WM_MOUSELEAVE.
|
|||
//
|
|||
// QS_POSTMESSAGE
|
|||
// If there is already a message in the queue, we need to process
|
|||
// it before we can process input.
|
|||
//
|
|||
// MWMO_INPUTAVAILABLE
|
|||
// This flag indicates that any input (new or old) is to be
|
|||
// reported.
|
|||
//
|
|||
|
|||
return MsgWaitForMultipleObjectsEx(0, null, 0, |
|||
QueueStatusFlags.QS_INPUT | QueueStatusFlags.QS_EVENT | QueueStatusFlags.QS_POSTMESSAGE, |
|||
MsgWaitForMultipleObjectsFlags.MWMO_INPUTAVAILABLE) == 0; |
|||
} |
|||
} |
|||
|
|||
public void RunLoop(CancellationToken cancellationToken) |
|||
{ |
|||
var result = 0; |
|||
while (!cancellationToken.IsCancellationRequested |
|||
&& (result = GetMessage(out var msg, IntPtr.Zero, 0, 0)) > 0) |
|||
{ |
|||
TranslateMessage(ref msg); |
|||
DispatchMessage(ref msg); |
|||
} |
|||
if (result < 0) |
|||
{ |
|||
Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.Win32Platform) |
|||
?.Log(this, "Unmanaged error in {0}. Error Code: {1}", nameof(RunLoop), Marshal.GetLastWin32Error()); |
|||
} |
|||
} |
|||
|
|||
public long Now => _clock.ElapsedMilliseconds; |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue