34 changed files with 2953 additions and 773 deletions
@ -0,0 +1,541 @@ |
|||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,203 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
|
||||
|
namespace Avalonia.Threading; |
||||
|
|
||||
|
public partial class Dispatcher |
||||
|
{ |
||||
|
private readonly DispatcherPriorityQueue _queue = new(); |
||||
|
private bool _signaled; |
||||
|
private DispatcherTimer? _backgroundTimer; |
||||
|
private const int MaximumTimeProcessingBackgroundJobs = 50; |
||||
|
|
||||
|
void RequestBackgroundProcessing() |
||||
|
{ |
||||
|
if (_backgroundTimer == null) |
||||
|
{ |
||||
|
_backgroundTimer = |
||||
|
new DispatcherTimer(this, DispatcherPriority.Send, |
||||
|
TimeSpan.FromMilliseconds(1)); |
||||
|
_backgroundTimer.Tick += delegate |
||||
|
{ |
||||
|
_backgroundTimer.Stop(); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
_backgroundTimer.IsEnabled = true; |
||||
|
} |
||||
|
|
||||
|
/// <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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal static void ResetForUnitTests() |
||||
|
{ |
||||
|
if (s_uiThread == null) |
||||
|
return; |
||||
|
var st = Stopwatch.StartNew(); |
||||
|
while (true) |
||||
|
{ |
||||
|
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() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
ExecuteJobsCore(); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
lock (InstanceLock) |
||||
|
_signaled = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void ExecuteJobsCore() |
||||
|
{ |
||||
|
int? 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 = Clock.TickCount; |
||||
|
|
||||
|
if (Clock.TickCount - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs) |
||||
|
{ |
||||
|
_signaled = true; |
||||
|
RequestBackgroundProcessing(); |
||||
|
return; |
||||
|
} |
||||
|
else |
||||
|
ExecuteJob(job); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool RequestProcessing() |
||||
|
{ |
||||
|
lock (InstanceLock) |
||||
|
{ |
||||
|
if (_queue.MaxPriority <= DispatcherPriority.Input) |
||||
|
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,171 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Threading; |
||||
|
|
||||
|
public partial class Dispatcher |
||||
|
{ |
||||
|
private List<DispatcherTimer> _timers = new(); |
||||
|
private long _timersVersion; |
||||
|
private bool _dueTimeFound; |
||||
|
private int _dueTimeInTicks; |
||||
|
private bool _isOsTimerSet; |
||||
|
|
||||
|
internal void UpdateOSTimer() |
||||
|
{ |
||||
|
if (!CheckAccess()) |
||||
|
{ |
||||
|
Post(UpdateOSTimer, DispatcherPriority.Send); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
lock (InstanceLock) |
||||
|
{ |
||||
|
if (!_hasShutdownFinished) // Dispatcher thread, does not technically need the lock to read
|
||||
|
{ |
||||
|
bool oldDueTimeFound = _dueTimeFound; |
||||
|
int oldDueTimeInTicks = _dueTimeInTicks; |
||||
|
_dueTimeFound = false; |
||||
|
_dueTimeInTicks = 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 - _dueTimeInTicks < 0) |
||||
|
{ |
||||
|
_dueTimeFound = true; |
||||
|
_dueTimeInTicks = timer.DueTimeInMs; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (_dueTimeFound) |
||||
|
{ |
||||
|
if (!_isOsTimerSet || !oldDueTimeFound || (oldDueTimeInTicks != _dueTimeInTicks)) |
||||
|
{ |
||||
|
_impl.UpdateTimer(Math.Max(1, _dueTimeInTicks)); |
||||
|
_isOsTimerSet = true; |
||||
|
} |
||||
|
} |
||||
|
else if (oldDueTimeFound) |
||||
|
{ |
||||
|
_impl.UpdateTimer(null); |
||||
|
_isOsTimerSet = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal void AddTimer(DispatcherTimer timer) |
||||
|
{ |
||||
|
lock (InstanceLock) |
||||
|
{ |
||||
|
if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
|
||||
|
{ |
||||
|
_timers.Add(timer); |
||||
|
_timersVersion++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
UpdateOSTimer(); |
||||
|
} |
||||
|
|
||||
|
internal void RemoveTimer(DispatcherTimer timer) |
||||
|
{ |
||||
|
lock (InstanceLock) |
||||
|
{ |
||||
|
if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
|
||||
|
{ |
||||
|
_timers.Remove(timer); |
||||
|
_timersVersion++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
UpdateOSTimer(); |
||||
|
} |
||||
|
|
||||
|
private void OnOSTimer() |
||||
|
{ |
||||
|
lock (InstanceLock) |
||||
|
{ |
||||
|
_impl.UpdateTimer(null); |
||||
|
_isOsTimerSet = false; |
||||
|
} |
||||
|
PromoteTimers(); |
||||
|
} |
||||
|
|
||||
|
internal void PromoteTimers() |
||||
|
{ |
||||
|
int currentTimeInTicks = Clock.TickCount; |
||||
|
try |
||||
|
{ |
||||
|
List<DispatcherTimer>? timers = null; |
||||
|
long timersVersion = 0; |
||||
|
|
||||
|
lock (InstanceLock) |
||||
|
{ |
||||
|
if (!_hasShutdownFinished) // Could be a non-dispatcher thread, lock to read
|
||||
|
{ |
||||
|
if (_dueTimeFound && _dueTimeInTicks - 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 |
||||
|
{ |
||||
|
UpdateOSTimer(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,159 +1,119 @@ |
|||||
using System; |
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Runtime.CompilerServices; |
||||
using System.Threading; |
using System.Threading; |
||||
using System.Threading.Tasks; |
|
||||
using Avalonia.Platform; |
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>
|
private readonly IDispatcherImpl _impl; |
||||
/// Provides services for managing work items on a thread.
|
internal IDispatcherClock Clock { get; } |
||||
/// </summary>
|
internal object InstanceLock { get; } = new(); |
||||
/// <remarks>
|
private bool _hasShutdownFinished; |
||||
/// In Avalonia, there is usually only a single <see cref="Dispatcher"/> in the application -
|
private readonly IControlledDispatcherImpl? _controlledImpl; |
||||
/// the one for the UI thread, retrieved via the <see cref="UIThread"/> property.
|
private static Dispatcher? s_uiThread; |
||||
/// </remarks>
|
private readonly IDispatcherImplWithPendingInput? _pendingInputImpl; |
||||
public class Dispatcher : IDispatcher |
|
||||
|
internal Dispatcher(IDispatcherImpl impl, IDispatcherClock clock) |
||||
{ |
{ |
||||
private readonly JobRunner _jobRunner; |
_impl = impl; |
||||
private IPlatformThreadingInterface? _platform; |
Clock = clock; |
||||
|
impl.Timer += OnOSTimer; |
||||
public static Dispatcher UIThread { get; } = |
impl.Signaled += Signaled; |
||||
new Dispatcher(AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>()); |
_controlledImpl = _impl as IControlledDispatcherImpl; |
||||
|
_pendingInputImpl = _impl as IDispatcherImplWithPendingInput; |
||||
public Dispatcher(IPlatformThreadingInterface? platform) |
} |
||||
{ |
|
||||
_platform = platform; |
public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher(); |
||||
_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(); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
private static Dispatcher CreateUIThreadDispatcher() |
||||
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority = default) |
{ |
||||
|
var impl = AvaloniaLocator.Current.GetService<IDispatcherImpl>(); |
||||
|
if (impl == null) |
||||
{ |
{ |
||||
_ = function ?? throw new ArgumentNullException(nameof(function)); |
var platformThreading = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>(); |
||||
return _jobRunner.InvokeAsync(function, priority).Unwrap(); |
if (platformThreading != null) |
||||
|
impl = new LegacyDispatcherImpl(platformThreading); |
||||
|
else |
||||
|
impl = new NullDispatcherImpl(); |
||||
} |
} |
||||
|
return new Dispatcher(impl, impl as IDispatcherClock ?? new DefaultDispatcherClock()); |
||||
|
} |
||||
|
|
||||
/// <inheritdoc/>
|
/// <summary>
|
||||
public void Post(Action action, DispatcherPriority priority = default) |
/// Checks that the current thread is the UI thread.
|
||||
{ |
/// </summary>
|
||||
_ = action ?? throw new ArgumentNullException(nameof(action)); |
public bool CheckAccess() => _impl?.CurrentThreadIsLoopThread ?? true; |
||||
_jobRunner.Post(action, priority); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
/// <summary>
|
||||
public void Post(SendOrPostCallback action, object? arg, DispatcherPriority priority = default) |
/// 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)); |
// Used to inline VerifyAccess.
|
||||
_jobRunner.Post(action, arg, priority); |
[DoesNotReturn] |
||||
} |
[MethodImpl(MethodImplOptions.NoInlining)] |
||||
|
static void ThrowVerifyAccess() |
||||
|
=> throw new InvalidOperationException("Call from invalid thread"); |
||||
|
|
||||
/// <summary>
|
ThrowVerifyAccess(); |
||||
/// 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); |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
/// <summary>
|
internal void Shutdown() |
||||
/// Allows unit tests to change the platform threading interface.
|
{ |
||||
/// </summary>
|
DispatcherOperation? operation = null; |
||||
internal void UpdateServices() |
_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>(); |
if(operation != null) |
||||
_jobRunner.UpdateServices(); |
|
||||
|
|
||||
if (_platform != 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,270 @@ |
|||||
|
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) |
||||
|
return TaskCompletionSource.Task.GetAwaiter().GetResult(); |
||||
|
throw new InvalidOperationException("Synchronous wait is only supported on non-UI threads"); |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 System; |
||||
using Avalonia.Reactive; |
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>
|
/// <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>
|
/// </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>
|
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
|
/// Creates a timer that uses the UI thread's Dispatcher2 to
|
||||
/// </summary>
|
/// process the timer event at the specified priority after the specified timeout.
|
||||
public DispatcherTimer() : this(DispatcherPriority.Background) |
/// </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>
|
Tick += callback; |
||||
/// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
|
Start(); |
||||
/// </summary>
|
} |
||||
/// <param name="priority">The priority to use.</param>
|
|
||||
public DispatcherTimer(DispatcherPriority priority) |
|
||||
{ |
|
||||
_priority = priority; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DispatcherTimer"/> class.
|
/// Gets the dispatcher this timer is associated with.
|
||||
/// </summary>
|
/// </summary>
|
||||
/// <param name="interval">The interval at which to tick.</param>
|
public Dispatcher Dispatcher |
||||
/// <param name="priority">The priority to use.</param>
|
{ |
||||
/// <param name="callback">The event to call when the timer ticks.</param>
|
get { return _dispatcher; } |
||||
public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback) : this(priority) |
} |
||||
{ |
|
||||
_priority = priority; |
|
||||
Interval = interval; |
|
||||
Tick += callback; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="DispatcherTimer"/> class.
|
/// Gets or sets whether the timer is running.
|
||||
/// </summary>
|
/// </summary>
|
||||
~DispatcherTimer() |
public bool IsEnabled |
||||
|
{ |
||||
|
get { return _isEnabled; } |
||||
|
|
||||
|
set |
||||
{ |
{ |
||||
if (_timer != null) |
lock (_instanceLock) |
||||
{ |
{ |
||||
Stop(); |
if (!value && _isEnabled) |
||||
|
{ |
||||
|
Stop(); |
||||
|
} |
||||
|
else if (value && !_isEnabled) |
||||
|
{ |
||||
|
Start(); |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
} |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Raised when the timer ticks.
|
/// Gets or sets the time between timer ticks.
|
||||
/// </summary>
|
/// </summary>
|
||||
public event EventHandler? Tick; |
public TimeSpan Interval |
||||
|
{ |
||||
|
get { return _interval; } |
||||
|
|
||||
/// <summary>
|
set |
||||
/// Gets or sets the interval at which the timer ticks.
|
|
||||
/// </summary>
|
|
||||
public TimeSpan Interval |
|
||||
{ |
{ |
||||
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.Clock.TickCount + (int)_interval.TotalMilliseconds; |
||||
|
updateOSTimer = true; |
||||
|
} |
||||
} |
} |
||||
|
|
||||
set |
if (updateOSTimer) |
||||
{ |
{ |
||||
bool enabled = IsEnabled; |
_dispatcher.UpdateOSTimer(); |
||||
Stop(); |
|
||||
_interval = value; |
|
||||
IsEnabled = enabled; |
|
||||
} |
} |
||||
} |
} |
||||
|
} |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets or sets a value indicating whether the timer is running.
|
/// Starts the timer.
|
||||
/// </summary>
|
/// </summary>
|
||||
public bool IsEnabled |
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) |
_operation.Abort(); |
||||
{ |
_operation = null; |
||||
Start(); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
Stop(); |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
/// <summary>
|
if (updateOSTimer) |
||||
/// Gets or sets user-defined data associated with the timer.
|
|
||||
/// </summary>
|
|
||||
public object? Tag |
|
||||
{ |
{ |
||||
get; |
_dispatcher.RemoveTimer(this); |
||||
set; |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
|
/// <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>
|
timer.Tick += (s, e) => |
||||
/// 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 }; |
if (!action()) |
||||
|
|
||||
timer.Tick += (s, e) => |
|
||||
{ |
{ |
||||
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>
|
timer.Tick += (s, e) => |
||||
/// 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); |
action(); |
||||
|
timer.Stop(); |
||||
var timer = new DispatcherTimer(priority) { Interval = interval }; |
}; |
||||
|
|
||||
|
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>
|
if (interval.TotalMilliseconds < 0) |
||||
/// Starts the timer.
|
throw new ArgumentOutOfRangeException("interval", "TimeSpan period must be greater than or equal to zero."); |
||||
/// </summary>
|
|
||||
public void Start() |
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 has already been restarted, e.g. Start was called form the Tick handler.
|
||||
_timer = threading.StartTimer(_priority, Interval, InternalTick); |
return; |
||||
|
} |
||||
|
|
||||
|
// BeginInvoke a new operation.
|
||||
|
_operation = _dispatcher.InvokeAsync(FireTick, DispatcherPriority.Inactive); |
||||
|
|
||||
|
DueTimeInMs = _dispatcher.Clock.TickCount + (int)_interval.TotalMilliseconds; |
||||
|
|
||||
|
if (_interval.TotalMilliseconds == 0 && _dispatcher.CheckAccess()) |
||||
|
{ |
||||
|
// shortcut - just promote the item now
|
||||
|
Promote(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_dispatcher.AddTimer(this); |
||||
} |
} |
||||
} |
} |
||||
|
} |
||||
|
|
||||
/// <summary>
|
internal void Promote() // called from Dispatcher
|
||||
/// Stops the timer.
|
{ |
||||
/// </summary>
|
lock (_instanceLock) |
||||
public void Stop() |
|
||||
{ |
{ |
||||
if (IsEnabled) |
// Simply promote the operation to it's desired priority.
|
||||
|
if (_operation != null) |
||||
{ |
{ |
||||
_timer!.Dispose(); |
_operation.Priority = _priority; |
||||
_timer = null; |
|
||||
} |
} |
||||
} |
} |
||||
|
} |
||||
|
|
||||
|
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>
|
// If we are still enabled, start the timer again.
|
||||
/// Raises the <see cref="Tick"/> event on the dispatcher thread.
|
if (_isEnabled) |
||||
/// </summary>
|
|
||||
private void InternalTick() |
|
||||
{ |
{ |
||||
Dispatcher.UIThread.EnsurePriority(_priority); |
Restart(); |
||||
Tick?.Invoke(this, EventArgs.Empty); |
|
||||
} |
} |
||||
} |
} |
||||
} |
|
||||
|
// 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 int DueTimeInMs { get; private set; } |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Threading; |
||||
|
|
||||
|
internal interface IDispatcherClock |
||||
|
{ |
||||
|
int TickCount { get; } |
||||
|
} |
||||
|
|
||||
|
internal class DefaultDispatcherClock : IDispatcherClock |
||||
|
{ |
||||
|
public int TickCount => Environment.TickCount; |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Threading; |
||||
|
|
||||
|
interface IDispatcherImpl |
||||
|
{ |
||||
|
|
||||
|
//IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick);
|
||||
|
|
||||
|
bool CurrentThreadIsLoopThread { get; } |
||||
|
|
||||
|
// Asynchronously triggers Signaled callback
|
||||
|
void Signal(); |
||||
|
event Action Signaled; |
||||
|
event Action Timer; |
||||
|
void UpdateTimer(int? dueTimeInTicks); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
interface IDispatcherImplWithPendingInput : IDispatcherImpl |
||||
|
{ |
||||
|
// Checks if dispatcher implementation can
|
||||
|
bool CanQueryPendingInput { get; } |
||||
|
// Checks if there is pending user input
|
||||
|
bool HasPendingInput { get; } |
||||
|
} |
||||
|
|
||||
|
interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput |
||||
|
{ |
||||
|
// Runs the event loop
|
||||
|
void RunLoop(CancellationToken token); |
||||
|
} |
||||
|
|
||||
|
internal class LegacyDispatcherImpl : IControlledDispatcherImpl |
||||
|
{ |
||||
|
private readonly IPlatformThreadingInterface _platformThreading; |
||||
|
private IDisposable? _timer; |
||||
|
|
||||
|
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 void UpdateTimer(int? dueTimeInTicks) |
||||
|
{ |
||||
|
_timer?.Dispose(); |
||||
|
_timer = null; |
||||
|
if (dueTimeInTicks.HasValue) |
||||
|
_timer = _platformThreading.StartTimer(DispatcherPriority.Send, |
||||
|
TimeSpan.FromMilliseconds(dueTimeInTicks.Value), |
||||
|
OnTick); |
||||
|
} |
||||
|
|
||||
|
private void OnTick() |
||||
|
{ |
||||
|
_timer?.Dispose(); |
||||
|
_timer = null; |
||||
|
Timer?.Invoke(); |
||||
|
} |
||||
|
|
||||
|
public bool CanQueryPendingInput => false; |
||||
|
public bool HasPendingInput => false; |
||||
|
public void RunLoop(CancellationToken token) => _platformThreading.RunLoop(token); |
||||
|
} |
||||
|
|
||||
|
class NullDispatcherImpl : IDispatcherImpl |
||||
|
{ |
||||
|
public bool CurrentThreadIsLoopThread => true; |
||||
|
|
||||
|
public void Signal() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public event Action? Signaled; |
||||
|
public event Action? Timer; |
||||
|
|
||||
|
public void UpdateTimer(int? dueTimeInTicks) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
#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, IDispatcherClock |
||||
|
{ |
||||
|
private readonly IAvnPlatformThreadingInterface _native; |
||||
|
private Thread? _loopThread; |
||||
|
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; |
||||
|
|
||||
|
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 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(int? dueTimeInTicks) |
||||
|
{ |
||||
|
var ms = dueTimeInTicks == null ? -1 : Math.Max(1, dueTimeInTicks.Value - TickCount); |
||||
|
_native.UpdateTimer(ms); |
||||
|
} |
||||
|
|
||||
|
public bool CanQueryPendingInput => true; |
||||
|
public bool HasPendingInput => _native.HasPendingInput() != 0; |
||||
|
|
||||
|
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 int TickCount => Environment.TickCount; |
||||
|
|
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
@ -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.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, IDispatcherClock |
||||
|
{ |
||||
|
private readonly IntPtr _messageWindow; |
||||
|
private static Thread? s_uiThread; |
||||
|
private IntPtr? _timerHandle; |
||||
|
private readonly TimerProc _timerDelegate; |
||||
|
public Win32DispatcherImpl(IntPtr messageWindow) |
||||
|
{ |
||||
|
_messageWindow = messageWindow; |
||||
|
s_uiThread = Thread.CurrentThread; |
||||
|
_timerDelegate = TimerProc; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
void TimerProc(IntPtr hWnd, uint uMsg, IntPtr nIdEvent, uint dwTime) => Timer?.Invoke(); |
||||
|
|
||||
|
public void UpdateTimer(int? dueTimeInTicks) |
||||
|
{ |
||||
|
if (_timerHandle.HasValue) |
||||
|
KillTimer(IntPtr.Zero, _timerHandle.Value); |
||||
|
if (dueTimeInTicks == null) |
||||
|
return; |
||||
|
|
||||
|
var interval = (uint)Math.Max(1, TickCount - dueTimeInTicks.Value); |
||||
|
|
||||
|
_timerHandle = SetTimer( |
||||
|
IntPtr.Zero, |
||||
|
IntPtr.Zero, |
||||
|
interval, |
||||
|
_timerDelegate); |
||||
|
} |
||||
|
|
||||
|
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 int TickCount => Environment.TickCount; |
||||
|
} |
||||
@ -0,0 +1,180 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Threading; |
||||
|
using Xunit; |
||||
|
namespace Avalonia.Base.UnitTests; |
||||
|
|
||||
|
public class DispatcherTests |
||||
|
{ |
||||
|
class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherClock, IDispatcherImplWithPendingInput |
||||
|
{ |
||||
|
public bool CurrentThreadIsLoopThread => true; |
||||
|
|
||||
|
public void Signal() => AskedForSignal = true; |
||||
|
|
||||
|
public event Action Signaled; |
||||
|
public event Action Timer; |
||||
|
public int? NextTimer { get; private set; } |
||||
|
public bool AskedForSignal { get; private set; } |
||||
|
|
||||
|
public void UpdateTimer(int? dueTimeInTicks) |
||||
|
{ |
||||
|
NextTimer = dueTimeInTicks; |
||||
|
} |
||||
|
|
||||
|
public int TickCount { get; set; } |
||||
|
|
||||
|
public void ExecuteSignal() |
||||
|
{ |
||||
|
if (!AskedForSignal) |
||||
|
return; |
||||
|
AskedForSignal = false; |
||||
|
Signaled?.Invoke(); |
||||
|
} |
||||
|
|
||||
|
public void ExecuteTimer() |
||||
|
{ |
||||
|
if (NextTimer == null) |
||||
|
return; |
||||
|
TickCount = NextTimer.Value; |
||||
|
Timer?.Invoke(); |
||||
|
} |
||||
|
|
||||
|
public bool CanQueryPendingInput => TestInputPending != null; |
||||
|
public bool HasPendingInput => TestInputPending == true; |
||||
|
public bool? TestInputPending { get; set; } |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[Fact] |
||||
|
public void DispatcherExecutesJobsAccordingToPriority() |
||||
|
{ |
||||
|
var impl = new SimpleDispatcherImpl(); |
||||
|
var disp = new Dispatcher(impl, impl); |
||||
|
var actions = new List<string>(); |
||||
|
disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background); |
||||
|
disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render); |
||||
|
disp.Post(()=>actions.Add("Input"), DispatcherPriority.Input); |
||||
|
Assert.True(impl.AskedForSignal); |
||||
|
impl.ExecuteSignal(); |
||||
|
Assert.Equal(new[] { "Render", "Input", "Background" }, actions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void DispatcherPreservesOrderWhenChangingPriority() |
||||
|
{ |
||||
|
var impl = new SimpleDispatcherImpl(); |
||||
|
var disp = new Dispatcher(impl, impl); |
||||
|
var actions = new List<string>(); |
||||
|
var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background); |
||||
|
var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input); |
||||
|
disp.Post(() => actions.Add("Render"), DispatcherPriority.Render); |
||||
|
|
||||
|
toPromote.Priority = DispatcherPriority.Render; |
||||
|
toPromote2.Priority = DispatcherPriority.Render; |
||||
|
|
||||
|
Assert.True(impl.AskedForSignal); |
||||
|
impl.ExecuteSignal(); |
||||
|
|
||||
|
Assert.Equal(new[] { "PromotedRender", "PromotedRender2", "Render" }, actions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached() |
||||
|
{ |
||||
|
var impl = new SimpleDispatcherImpl(); |
||||
|
var disp = new Dispatcher(impl, impl); |
||||
|
var actions = new List<int>(); |
||||
|
for (var c = 0; c < 10; c++) |
||||
|
{ |
||||
|
var itemId = c; |
||||
|
disp.Post(() => |
||||
|
{ |
||||
|
actions.Add(itemId); |
||||
|
impl.TickCount += 20; |
||||
|
}, DispatcherPriority.Background); |
||||
|
} |
||||
|
|
||||
|
Assert.False(impl.AskedForSignal); |
||||
|
Assert.NotNull(impl.NextTimer); |
||||
|
|
||||
|
impl.ExecuteTimer(); |
||||
|
Assert.True(impl.AskedForSignal); |
||||
|
Assert.Null(impl.NextTimer); |
||||
|
|
||||
|
for (var c = 0; c < 4; c++) |
||||
|
{ |
||||
|
if (impl.NextTimer != null) |
||||
|
impl.ExecuteTimer(); |
||||
|
Assert.True(impl.AskedForSignal); |
||||
|
impl.ExecuteSignal(); |
||||
|
var expectedCount = (c + 1) * 3; |
||||
|
if (c == 3) |
||||
|
expectedCount = 10; |
||||
|
|
||||
|
Assert.Equal(Enumerable.Range(0, expectedCount), actions); |
||||
|
Assert.False(impl.AskedForSignal); |
||||
|
if (c < 3) |
||||
|
{ |
||||
|
Assert.True(impl.NextTimer > impl.TickCount); |
||||
|
} |
||||
|
else |
||||
|
Assert.Null(impl.NextTimer); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[Fact] |
||||
|
public void DispatcherStopsItemProcessingWhenInputIsPending() |
||||
|
{ |
||||
|
var impl = new SimpleDispatcherImpl(); |
||||
|
impl.TestInputPending = false; |
||||
|
var disp = new Dispatcher(impl, impl); |
||||
|
var actions = new List<int>(); |
||||
|
for (var c = 0; c < 10; c++) |
||||
|
{ |
||||
|
var itemId = c; |
||||
|
disp.Post(() => |
||||
|
{ |
||||
|
actions.Add(itemId); |
||||
|
if (itemId == 0 || itemId == 3 || itemId == 7) |
||||
|
impl.TestInputPending = true; |
||||
|
}, DispatcherPriority.Background); |
||||
|
} |
||||
|
Assert.False(impl.AskedForSignal); |
||||
|
Assert.NotNull(impl.NextTimer); |
||||
|
|
||||
|
impl.ExecuteTimer(); |
||||
|
Assert.True(impl.AskedForSignal); |
||||
|
Assert.Null(impl.NextTimer); |
||||
|
|
||||
|
for (var c = 0; c < 4; c++) |
||||
|
{ |
||||
|
if (impl.NextTimer != null) |
||||
|
impl.ExecuteTimer(); |
||||
|
Assert.True(impl.AskedForSignal); |
||||
|
impl.ExecuteSignal(); |
||||
|
var expectedCount = c switch |
||||
|
{ |
||||
|
0 => 1, |
||||
|
1 => 4, |
||||
|
2 => 8, |
||||
|
3 => 10 |
||||
|
}; |
||||
|
|
||||
|
Assert.Equal(Enumerable.Range(0, expectedCount), actions); |
||||
|
Assert.False(impl.AskedForSignal); |
||||
|
if (c < 3) |
||||
|
{ |
||||
|
Assert.True(impl.NextTimer > impl.TickCount); |
||||
|
impl.TickCount = impl.NextTimer.Value + 1; |
||||
|
} |
||||
|
else |
||||
|
Assert.Null(impl.NextTimer); |
||||
|
|
||||
|
impl.TestInputPending = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue