yield and check cancellation token.
I was reading through some code recently and ran across an asynchronous method that
immediately called cancellationToken.ThrowIfCancellationRequested(). Nothing wrong
with that. It’s a good practice to check for cancellation at the start of a method. But,
I rarely actually do unless I’m about to start a long-running operation. I almost always
call await Task.Yield() at the beginning of my async methods. This allows the calling
method to continue on its context and the new method to run on its own context (a
polite way to force a method to run async).
That made me think it would be nice to have a method that would check for cancellation and yield at the same time. Basically, just put them in a single function:
public static async Task YieldCancelCheck(CancellationToken cancellationToken)
{
await Task.Yield();
cancellationToken.ThrowIfCancellationRequested();
}
That would work, but it would be nice to know where the cancellation happened. This
function would always be at the top of the call stack. It would be nice to have the
caller’s information at the top of the stack instead of digging for it. I thought about
trying to use ExceptionDispatchInfo, but that would need to be in the calling function,
defeating the purpose. Passing the caller’s information to the function would seem to
be the best solution. This way, the function can throw an OperationCanceledException
with the caller’s information in the message.
public static async Task YieldCancelCheck(
CancellationToken cancellationToken,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
await Task.Yield();
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException(
$"Task was canceled at {memberName} in {filePath} at line {lineNumber}.",
cancellationToken);
}
}
That’s pretty straight forward. The if and yield are unrelated so we could switch
them around. It doesn’t really matter unless the token is cancelled. Which should be
very rare I would think. If this is a frequent occurrence or concern, you may want to
consider if you would prefer to check for cancellation before switching contexts or
if you would prefer to have a cancellation exception on its own context. It’s a small
function - you could have one of each if you wanted :).
I tried thinking of other things to do with this function, and the first thing that
came to mind was to make it an extension method on CancellationToken. That way it
would be a little more natural to call - not having to pass the token in. That way, we
can await a token.Yield() instead of Task.Yield(). It also seems more logical to
me. Thinking of it as yielding on the token, not the task.
The other thing I thought of was to implement the function the same way Yield()
itself is implemented. That way we wouldn’t need to call Task.Yield() inside the
method. I should probably go ahead and say that this should not be done and should
be put in the “bad idea” / “stupid C# trick” category. The native Yield() method
is synchronous (as such, it doesn’t create a state machine), but returns
YieldAwaitable so it can be awaited. So, we drop the async, return
YieldAwaitable, get rid of the Yield(), and return a default(YieldAwaitable).
Easy enough, right?
public static YieldAwaitable Yield(
CancellationToken cancellationToken,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException(
$"Task was canceled at {memberName} in {filePath} at line {lineNumber}.",
cancellationToken);
}
return default;
}
Never let CompilerServices internal comments like intended for compiler use only
stop you from using it. Live dangerously! I mean, what could possibly go wrong?
Insert stupid code trick disclaimer - Remember, this is just a fun little experiment. Don’t use this in production code.