Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is ICriticalNotifyCompletion for?

Tags:

c#

async-await

I'm trying to understand how the C# async mechanisms actually works and one source of confusion is the ICriticalNotifyCompletion interface.

The interface provides two methods: OnCompleted(Action), inherited from INotifyCompletion and UnsafeOnCompleted(Action). The documentation has the exact same description for both methods: "Schedules the continuation action that's invoked when the instance completes.".

Only difference is that a remark on UnsafeOnCompleted(Action) states that it doesn't have to "propagate ExecutionContext information", whatever that means. It is nowhere explicitly stated that OnCompleted(Action) has to "propagate ExecutionContext information".

The documentation on awaitable expressions states that UnsafeOnCompleted(Action) is called if the awaiter implements ICriticalNotifyCompletion. So implementing ICriticalNotifyCompletion makes OnCompleted(Action) redundant?

Why would an awaiter implement ICriticalNotifyCompletion? What are the implications of TaskAwaiter implementing ICriticalNotifyCompletion? What if it didn't?

like image 891
TrayMan Avatar asked Jan 01 '21 12:01

TrayMan


People also ask

What is an Awaiter C#?

An object is an awaiter if: It implements INotifyCompletion or ICriticalNotifyCompletion interface; It has an IsCompleted, which has a getter and returns a Boolean; it has a GetResult() method, which returns void, or a result.


1 Answers

"propagate ExecutionContext information", whatever that means

There's actually no good exhaustive definition for this, and I certainly won't try to provide one because I know I'll miss something important. I do know, though, that flowing ExecutionContext is necessary for security reasons - this is why all methods that don't flow the context use the Unsafe naming convention. Stephen Toub has this to say:

ExecutionContext is all about “ambient” information, meaning that it stores data relevant to the current environment or “context” in which you’re running... one of the contexts contained by ExecutionContext is SecurityContext, which maintains information like the current “principal” and information about code access security (CAS) denies and permits.

So the first thing to recognize is that the ExecutionContext must be flowed. The next piece of the puzzle is again described by Stephen Toub. For historical context, this description is when async/await was still a prerelease (but publicly available) technology:

many folks didn’t realize that their awaiters needed to flow ExecutionContext in order to ensure context flowed across await points... [So] we’ve modified the async method builders in the Framework (e.g. AsyncTaskMethodBuilder)... The builders now themselves flow ExecutionContext across await points, taking that responsibility away from the awaiters.

Originally, the awaiters would flow ExecutionContext, but this was changed before the official async/await release so that the builders flow ExecutionContext. Naturally, this means the awaiters no longer have to flow ExecutionContext; if they did, asynchronous code would end up flowing it twice (where a "flow" is a "capture" followed by an "execute delegate in this captured context").

Now there's enough information to answer these questions:

Why would an awaiter implement ICriticalNotifyCompletion? What are the implications of TaskAwaiter implementing ICriticalNotifyCompletion? What if it didn't?

If an awaiter doesn't implement ICriticalNotifyCompletion, then an await using that code will end up flowing the ExecutionContext twice (one by the awaiter, and once by the async method builder). It's not going to break anything; it'll just be less efficient than it could be.

So implementing ICriticalNotifyCompletion makes OnCompleted(Action) redundant?

Not quite. Again, delegating to Stephen Toub:

If you’re building an assembly with AllowPartiallyTrustedCallersAttribute (APTCA) applied to it, you need to ensure that any publicly exposed APIs from your assembly correctly flow ExecutionContext across async points… failure to do so can be a big security hole. As awaiter types will often be implemented in APTCA assemblies, and since OnCompleted could be called directly by a user (even though it’s really meant to be used by the compiler), OnCompleted needs to flow ExecutionContext... we also have UnsafeOnCompleted, which doesn’t need to flow ExecutionContext, but which is also marked as SecurityCritical, such that partially trusted code can’t call it.

with the conclusion:

If you’re implementing your own awaiter, whenever possible implement both INotifyCompletion and ICriticalNotifyCompletion, flowing ExecutionContext in the former and not flowing it in the latter. The only good reason not to implement both is if you’re implementing an awaiter in a situation where you can’t flow ExecutionContext, e.g. where your awaiter is partially trusted or where you otherwise don’t have the ability to use ExecutionContext, or where the APIs on which your awaiter relies doesn’t give you any option as to whether to flow context or not… in such cases, you can just implement INotifyCompletion.

I would modify this conclusion only slightly. In the almost-a-decade since the above was written, I would say that it is not common to use APTCA assemblies. I.e., .NET Core has no support for partial trust at all. For .NET Core, I believe you could say that OnCompleted is redundant. However, this distinction is still important in the .NET Framework world, where OnCompleted is necessary for awaiters in partial-trust assemblies.

So I would say: When implementing an awaiter, always implement ICriticalNotifyCompletion if you can implement it (i.e., without flowing ExecutionContext). Otherwise, just keep the regular OnCompleted implementation.

like image 178
Stephen Cleary Avatar answered Oct 19 '22 02:10

Stephen Cleary