Я пытаюсь понять, как на самом деле работают асинхронные механизмы C#, и одним из источников путаницы является интерфейс ICriticalNotifyCompletion.

Интерфейс предоставляет два метода: OnCompleted(Action), унаследованный от INotifyCompletion и UnsafeOnCompleted(Action). В документации для обоих методов приводится точно такое же описание: "Планирует действие продолжения, которое вызывается при завершении экземпляра.".

Единственное отличие заключается в том, что в примечании к UnsafeOnCompleted(Action) говорится, что он не должен "распространять информацию ExecutionContext", что бы это ни значило. Нигде явно не указано, что OnCompleted(Action) должен "распространять информацию о контексте выполнения".

В документации по ожидаемым выражениям говорится, что UnsafeOnCompleted(Action) вызывается, если awaiter реализует ICriticalNotifyCompletion. Значит, реализация ICriticalNotifyCompletion делает OnCompleted(Action) избыточным?

Зачем awaiter реализовывать ICriticalNotifyCompletion? Каковы последствия того, что TaskAwaiter реализует ICriticalNotifyCompletion? А что если нет?

TrayMan

Ответов: 1

Ответы (1)

"распространять информацию ExecutionContext", что бы это ни значило

На самом деле нет хорошего исчерпывающего определения для этого, и я, конечно, не буду пытаться дать его, потому что знаю, что упущу что-то важное. Однако я знаю, что передача ExecutionContext необходима по соображениям безопасности - вот почему все методы, которые не передают контекст, используют соглашение об именовании Unsafe. Стивен Тоуб говорит следующее:

ExecutionContext - это все об "окружающей" информации, то есть он хранит данные, относящиеся к текущей среде или "контексту", в котором вы работаете... Одним из контекстов, содержащихся в ExecutionContext, является SecurityContext, который хранит такую информацию, как текущий "принципал" и информацию о запретах и разрешениях безопасности доступа к коду (CAS).

Так что первое, что следует признать, это то, что ExecutionContext должен быть обтекаемым. Следующая часть головоломки снова описана Стивеном Тоубом. Для исторического контекста, это описание относится к тому времени, когда async/await были еще предварительной (но общедоступной) технологией:

Многие не понимали, что их ожидающие методы должны передавать ExecutionContext, чтобы обеспечить передачу контекста между точками ожидания... [Поэтому] мы изменили конструкторы асинхронных методов во Framework (например, AsyncTaskMethodBuilder)... Теперь построители сами передают ExecutionContext через точки ожидания, снимая эту ответственность с ожидающих.

Изначально ожидающие должны были передавать ExecutionContext, но это было изменено до официального выпуска async/await, так что строители передают ExecutionContext. Естественно, это означает, что ожидающим больше не нужно передавать ExecutionContext; если бы они это делали, асинхронный код в итоге передавал бы его дважды (где "поток" - это "захват", за которым следует "выполнение делегата в этом захваченном контексте")

.

Сейчас есть достаточно информации, чтобы ответить на эти вопросы:

Зачем awaiter реализовывать ICriticalNotifyCompletion? Каковы последствия того, что TaskAwaiter реализует ICriticalNotifyCompletion? А что если нет?

Если awaiter не реализует ICriticalNotifyCompletion, то await, использующий этот код, в конечном итоге будет дважды передавать ExecutionContext (один раз awaiter, а другой - построитель метода async). Это ничего не сломает; это просто будет менее эффективно, чем могло бы быть.

Так реализация ICriticalNotifyCompletion делает OnCompleted(Action) излишним?

Не совсем. Опять же, делегируя Стивену Тоубу:

Если вы собираете сборку с примененным к ней атрибутом AllowPartiallyTrustedCallersAttribute (APTCA), вам необходимо убедиться, что все публично открытые API из вашей сборки правильно передают ExecutionContext через точки асинхронизации... неспособность сделать это может стать большой дырой в безопасности. Поскольку типы awaiter часто реализуются в сборках APTCA, и поскольку OnCompleted может быть вызван непосредственно пользователем (хотя на самом деле он предназначен для компилятора), OnCompleted должен передавать ExecutionContext... У нас также есть UnsafeOnCompleted, которому не нужно передавать ExecutionContext, но который также помечен как SecurityCritical, так что частично доверенный код не может его вызвать.

.

с выводом:

Если вы реализуете свой собственный awaiter, по возможности реализуйте как INotifyCompletion, так и ICriticalNotifyCompletion, передавая ExecutionContext в первом случае и не передавая его во втором. Единственная веская причина не реализовывать оба варианта - это если вы реализуете awaiter в ситуации, когда вы не можете передавать ExecutionContext, например, когда ваш awaiter частично доверен или когда у вас нет возможности использовать ExecutionContext, или когда API, на который опирается ваш awaiter, не дает вам никакого выбора, передавать контекст или нет... в таких случаях вы можете просто реализовать INotifyCompletion.

.

Я бы лишь немного изменил этот вывод. За почти десятилетие, прошедшее с тех пор, как было написано выше, я бы сказал, что использование сборок APTCA не распространено. То есть, в .NET Core вообще нет поддержки частичного доверия. Для .NET Core, я думаю, можно сказать, что OnCompleted является избыточным. Однако это различие все еще важно в мире .NET Framework, где OnCompleted необходим для ожидающих в сборках с частичным доверием.

Так что я бы сказал: При реализации awaiter всегда реализуйте ICriticalNotifyCompletion, если вы можете его реализовать (т.е. без перетекания ExecutionContext). В противном случае просто сохраните обычную реализацию OnCompleted.

2022 WebDevInsider