dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.26k stars 4.73k forks source link

[API Proposal]: CancellationToken.UnsafeRegisterWithoutImmediateInvoke #107094

Open timcassell opened 2 months ago

timcassell commented 2 months ago

Background and motivation

I need to register a listener to the token while holding a non-reentrant lock (like SpinLock). If the callback is synchronously invoked due to the token having been canceled, it will cause a deadlock. It's impossible to simply check the IsCancellationRequested property before registering, because it could still be canceled on another thread, causing a race condition.

API Proposal

namespace System.Threading;

public struct CancellationToken
{
    public CancellationTokenRegistration UnsafeRegisterWithoutImmediateInvoke(Action<object?> callback, object? state, out bool alreadyCanceled);
    public CancellationTokenRegistration UnsafeRegisterWithoutImmediateInvoke(Action<object?, CancellationToken> callback, object? state, out bool alreadyCanceled);
}

alreadyCanceled is set to true if the callback would have been invoked using the existing register APIs.

API Usage

_spinLock.Enter();
var obj = new MyAsyncObject(this);
obj._registration = cancellationToken.UnsafeRegisterWithoutImmediateInvoke(o => ((MyAsyncObject) o).Cancel(), obj, out bool alreadyCanceled);
if (!alreadyCanceled)
{
    _queue.Enqueue(obj);
}
_spinLock.Exit();

Alternative Designs

Alternative 1 - use a more expensive reentrant lock.

Alternative 2 - use a [ThreadStatic] field to determine if the callback is invoked synchronously.

[ThreadStatic] private static bool ts_hookingUpCancelation;

// ...

_spinLock.Enter();
var obj = new MyAsyncObject(this);
ts_hookingUpCancelation = true;
obj._registration = cancellationToken.UnsafeRegister(o =>
{
    if (ts_hookingUpCancelation)
    {
        ts_hookingUpCancelation = false;
        return;
    }
    ((MyAsyncObject) o).Cancel();
}, obj);
if (!ts_hookingUpCancelation)
{
    _queue.Enqueue(obj);
}
else
{
    ts_hookingUpCancelation = false;
}
_spinLock.Exit();

Risks

No response

dotnet-policy-service[bot] commented 2 months ago

Tagging subscribers to this area: @mangod9 See info in area-owners.md if you want to be subscribed.