You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
13 KiB
375 lines
13 KiB
5 years ago
|
// CancellationTokenSource.cs
|
||
|
//
|
||
|
// Authors:
|
||
|
// Jérémie "Garuma" Laval <jeremie.laval@gmail.com>
|
||
|
// Marek Safar (marek.safar@gmail.com)
|
||
|
// Alfonso J. Ramos (theraot@gmail.com)
|
||
|
//
|
||
|
// Copyright (c) 2009 Jérémie "Garuma" Laval
|
||
|
// Copyright 2011 Xamarin, Inc (http://www.xamarin.com)
|
||
|
//
|
||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
// of this software and associated documentation files (the "Software"), to deal
|
||
|
// in the Software without restriction, including without limitation the rights
|
||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
// copies of the Software, and to permit persons to whom the Software is
|
||
|
// furnished to do so, subject to the following conditions:
|
||
|
//
|
||
|
// The above copyright notice and this permission notice shall be included in
|
||
|
// all copies or substantial portions of the Software.
|
||
|
//
|
||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
// THE SOFTWARE.
|
||
|
|
||
|
#if NET20 || NET30 || NET35 || !NET_4_6
|
||
|
|
||
|
using System.Collections.Generic;
|
||
|
using LinqInternal.Collections.ThreadSafe;
|
||
|
|
||
|
namespace System.Threading
|
||
|
{
|
||
|
public class CancellationTokenSource : IDisposable
|
||
|
{
|
||
|
internal static readonly CancellationTokenSource CanceledSource = new CancellationTokenSource(); // Leaked
|
||
|
internal static readonly CancellationTokenSource NoneSource = new CancellationTokenSource(); // Leaked
|
||
|
private static readonly Action<CancellationTokenSource> _timerCallback;
|
||
|
private readonly ManualResetEvent _handle;
|
||
|
private SafeDictionary<CancellationTokenRegistration, Action> _callbacks;
|
||
|
private int _cancelRequested;
|
||
|
private int _currentId = int.MaxValue;
|
||
|
private int _disposeRequested;
|
||
|
private CancellationTokenRegistration[] _linkedTokens;
|
||
|
private LinqInternal.Threading.Timeout<CancellationTokenSource> _timeout;
|
||
|
|
||
|
static CancellationTokenSource()
|
||
|
{
|
||
|
CanceledSource._cancelRequested = 1;
|
||
|
_timerCallback = cancellationTokenSource =>
|
||
|
{
|
||
|
var callbacks = cancellationTokenSource._callbacks;
|
||
|
if (callbacks != null)
|
||
|
{
|
||
|
cancellationTokenSource.CancelExtracted(false, callbacks, true);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
public CancellationTokenSource()
|
||
|
{
|
||
|
_callbacks = new SafeDictionary<CancellationTokenRegistration, Action>();
|
||
|
_handle = new ManualResetEvent(false);
|
||
|
}
|
||
|
|
||
|
public CancellationTokenSource(int millisecondsDelay)
|
||
|
: this()
|
||
|
{
|
||
|
if (millisecondsDelay < -1)
|
||
|
{
|
||
|
throw new ArgumentOutOfRangeException("millisecondsDelay");
|
||
|
}
|
||
|
if (millisecondsDelay != Timeout.Infinite)
|
||
|
{
|
||
|
_timeout = new LinqInternal.Threading.Timeout<CancellationTokenSource>(_timerCallback, millisecondsDelay, this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public CancellationTokenSource(TimeSpan delay)
|
||
|
: this(CheckTimeout(delay))
|
||
|
{
|
||
|
//Empty
|
||
|
}
|
||
|
|
||
|
public bool IsCancellationRequested
|
||
|
{
|
||
|
get { return _cancelRequested == 1; }
|
||
|
}
|
||
|
|
||
|
public CancellationToken Token
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
CheckDisposed();
|
||
|
return new CancellationToken(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal WaitHandle WaitHandle
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
CheckDisposed();
|
||
|
return _handle;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static CancellationTokenSource CreateLinkedTokenSource(CancellationToken token1, CancellationToken token2)
|
||
|
{
|
||
|
return CreateLinkedTokenSource(new[] { token1, token2 });
|
||
|
}
|
||
|
|
||
|
public static CancellationTokenSource CreateLinkedTokenSource(params CancellationToken[] tokens)
|
||
|
{
|
||
|
if (tokens == null)
|
||
|
{
|
||
|
throw new ArgumentNullException("tokens");
|
||
|
}
|
||
|
if (tokens.Length == 0)
|
||
|
{
|
||
|
throw new ArgumentException("Empty tokens array");
|
||
|
}
|
||
|
var src = new CancellationTokenSource();
|
||
|
Action action = src.SafeLinkedCancel;
|
||
|
var registrations = new List<CancellationTokenRegistration>(tokens.Length);
|
||
|
foreach (var token in tokens)
|
||
|
{
|
||
|
if (token.CanBeCanceled)
|
||
|
{
|
||
|
registrations.Add(token.Register(action));
|
||
|
}
|
||
|
}
|
||
|
src._linkedTokens = registrations.ToArray();
|
||
|
return src;
|
||
|
}
|
||
|
|
||
|
public void Cancel()
|
||
|
{
|
||
|
Cancel(false);
|
||
|
}
|
||
|
|
||
|
public void Cancel(bool throwOnFirstException)
|
||
|
{
|
||
|
// If throwOnFirstException is true we throw exception as soon as they appear otherwise we aggregate them
|
||
|
var callbacks = CheckDisposedGetCallbacks();
|
||
|
CancelExtracted(throwOnFirstException, callbacks, false);
|
||
|
}
|
||
|
|
||
|
public void CancelAfter(TimeSpan delay)
|
||
|
{
|
||
|
CancelAfter(CheckTimeout(delay));
|
||
|
}
|
||
|
|
||
|
public void CancelAfter(int millisecondsDelay)
|
||
|
{
|
||
|
if (millisecondsDelay < -1)
|
||
|
{
|
||
|
throw new ArgumentOutOfRangeException("millisecondsDelay");
|
||
|
}
|
||
|
CheckDisposed();
|
||
|
if (Thread.VolatileRead(ref _cancelRequested) == 0 && millisecondsDelay != Timeout.Infinite)
|
||
|
{
|
||
|
if (_timeout == null)
|
||
|
{
|
||
|
// Have to be careful not to create secondary background timer
|
||
|
var newTimer = new LinqInternal.Threading.Timeout<CancellationTokenSource>(_timerCallback, Timeout.Infinite, this);
|
||
|
var oldTimer = Interlocked.CompareExchange(ref _timeout, newTimer, null);
|
||
|
if (!ReferenceEquals(oldTimer, null))
|
||
|
{
|
||
|
newTimer.Cancel();
|
||
|
}
|
||
|
_timeout.Change(millisecondsDelay);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void Dispose()
|
||
|
{
|
||
|
Dispose(true);
|
||
|
}
|
||
|
|
||
|
internal void CheckDisposed()
|
||
|
{
|
||
|
if (Thread.VolatileRead(ref _disposeRequested) == 1)
|
||
|
{
|
||
|
throw new ObjectDisposedException(GetType().Name);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal SafeDictionary<CancellationTokenRegistration, Action> CheckDisposedGetCallbacks()
|
||
|
{
|
||
|
var result = _callbacks;
|
||
|
if (result == null || Thread.VolatileRead(ref _disposeRequested) == 1)
|
||
|
{
|
||
|
throw new ObjectDisposedException(GetType().Name);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
internal CancellationTokenRegistration Register(Action callback, bool useSynchronizationContext)
|
||
|
{
|
||
|
// NOTICE this method has no null check
|
||
|
var callbacks = CheckDisposedGetCallbacks();
|
||
|
var tokenReg = new CancellationTokenRegistration(Interlocked.Decrement(ref _currentId), this);
|
||
|
// If the source is already canceled run the callback inline.
|
||
|
// if not, we try to add it to the queue and if it is currently being processed.
|
||
|
// we try to execute it back ourselves to be sure the callback is ran.
|
||
|
if (Thread.VolatileRead(ref _cancelRequested) == 1)
|
||
|
{
|
||
|
callback();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Capture execution contexts if the callback may not run inline.
|
||
|
if (useSynchronizationContext)
|
||
|
{
|
||
|
var capturedSyncContext = SynchronizationContext.Current;
|
||
|
var originalCallback = callback;
|
||
|
callback = () => capturedSyncContext.Send(_ => originalCallback(), null);
|
||
|
}
|
||
|
callbacks.TryAdd(tokenReg, callback);
|
||
|
// Check if the source was just canceled and if so, it may be that it executed the callbacks except the one just added...
|
||
|
// So try to inline the callback
|
||
|
if (Thread.VolatileRead(ref _cancelRequested) == 1 && callbacks.Remove(tokenReg, out callback))
|
||
|
{
|
||
|
callback();
|
||
|
}
|
||
|
}
|
||
|
return tokenReg;
|
||
|
}
|
||
|
|
||
|
internal bool RemoveCallback(CancellationTokenRegistration reg)
|
||
|
{
|
||
|
// Ignore call if the source has been disposed
|
||
|
if (Thread.VolatileRead(ref _disposeRequested) == 0)
|
||
|
{
|
||
|
var callbacks = _callbacks;
|
||
|
if (callbacks != null)
|
||
|
{
|
||
|
Action dummy;
|
||
|
return callbacks.Remove(reg, out dummy);
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
protected virtual void Dispose(bool disposing)
|
||
|
{
|
||
|
if (disposing && Interlocked.CompareExchange(ref _disposeRequested, 1, 0) == 0)
|
||
|
{
|
||
|
if (Thread.VolatileRead(ref _cancelRequested) == 0)
|
||
|
{
|
||
|
UnregisterLinkedTokens();
|
||
|
_callbacks = null;
|
||
|
}
|
||
|
var timer = Interlocked.Exchange(ref _timeout, null);
|
||
|
if (timer != null)
|
||
|
{
|
||
|
timer.Cancel();
|
||
|
}
|
||
|
_handle.Close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static int CheckTimeout(TimeSpan delay)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
return checked((int)delay.TotalMilliseconds);
|
||
|
}
|
||
|
catch (OverflowException)
|
||
|
{
|
||
|
throw new ArgumentOutOfRangeException("delay");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static void RunCallback(bool throwOnFirstException, Action callback, ref List<Exception> exceptions)
|
||
|
{
|
||
|
// NOTICE this method has no null check
|
||
|
if (throwOnFirstException)
|
||
|
{
|
||
|
callback();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
callback();
|
||
|
}
|
||
|
catch (Exception exception)
|
||
|
{
|
||
|
if (ReferenceEquals(exceptions, null))
|
||
|
{
|
||
|
exceptions = new List<Exception>();
|
||
|
}
|
||
|
exceptions.Add(exception);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void CancelExtracted(bool throwOnFirstException, SafeDictionary<CancellationTokenRegistration, Action> callbacks, bool ignoreDisposedException)
|
||
|
{
|
||
|
if (Interlocked.CompareExchange(ref _cancelRequested, 1, 0) == 0)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
// The CancellationTokenSource may have been disposed jusst before this call
|
||
|
_handle.Set();
|
||
|
}
|
||
|
catch (ObjectDisposedException)
|
||
|
{
|
||
|
if (!ignoreDisposedException)
|
||
|
{
|
||
|
throw;
|
||
|
}
|
||
|
}
|
||
|
UnregisterLinkedTokens();
|
||
|
List<Exception> exceptions = null;
|
||
|
try
|
||
|
{
|
||
|
var id = _currentId;
|
||
|
do
|
||
|
{
|
||
|
Action callback;
|
||
|
var checkId = id;
|
||
|
if (callbacks.Remove(id, registration => registration.Equals(checkId, this), out callback) && callback != null)
|
||
|
{
|
||
|
RunCallback(throwOnFirstException, callback, ref exceptions);
|
||
|
}
|
||
|
} while (id++ != int.MaxValue);
|
||
|
}
|
||
|
finally
|
||
|
{
|
||
|
// Whatever was added after the cancellation process started, it should run inline in Register... if they don't, handle then here.
|
||
|
foreach (
|
||
|
var callback in
|
||
|
callbacks.RemoveWhereKeyEnumerable(_ => true))
|
||
|
{
|
||
|
RunCallback(throwOnFirstException, callback, ref exceptions);
|
||
|
}
|
||
|
}
|
||
|
if (exceptions != null)
|
||
|
{
|
||
|
throw new AggregateException(exceptions);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void SafeLinkedCancel()
|
||
|
{
|
||
|
var callbacks = _callbacks;
|
||
|
if (callbacks == null || Thread.VolatileRead(ref _disposeRequested) == 1)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
CancelExtracted(false, callbacks, true);
|
||
|
}
|
||
|
|
||
|
private void UnregisterLinkedTokens()
|
||
|
{
|
||
|
var registrations = Interlocked.Exchange(ref _linkedTokens, null);
|
||
|
if (!ReferenceEquals(registrations, null))
|
||
|
{
|
||
|
foreach (var linked in registrations)
|
||
|
{
|
||
|
linked.Dispose();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|