Может ли компилятор С# различать границы ввода-вывода и вычислительные задачи?
Рассмотрим фрагмент кода, например:
public async Task<Bitmap> DownloadDataAndRenderImageAsync(
CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}
Первый из шагов этого метода - работа с привязкой ввода/вывода, где вторая, вычислительная.
Когда мы полагаемся на компилятор для создания правильного целевого кода для этой асинхронной операции, что делает компилятор?
В частности, знает ли он, что первый из них связан с I/O, поэтому он должен использовать класс TaskCompletionSource<T>
, чтобы между потоком и задачей не было сродства, а для второго оно может использовать любые методов, таких как Run
или StartNew
или Start
, чтобы запланировать задачу в потоке пула потоков?
Ответы
Ответ 1
Нет. В приведенном примере компилятор будет использовать TaskCompletionSource<T>
(косвенно) для общей асинхронной операции (DownloadDataAndRenderImageAsync
). Это до двух методов, которые призваны решать, как они собираются вернуть соответствующую задачу.
Может быть, DownloadImageDataAsync
сам по себе является async
, который делегирует еще несколько асинхронных операций ввода-вывода. Может быть, RenderAsync
вызывает Task.Run
. Это как детали реализации, которые компилятор вообще не интересует при компиляции DownloadDataAndRenderImageAsync
.
Ответ 2
Когда мы полагаемся на компилятор для создания правильного целевого кода для этой асинхронной операции, что делает компилятор?
В примере, который вы даете компилятору, известно, что DownloadImageDataAsync и RenderAsync - это методы, возвращающие awaitables. Ожидаемые объекты - это объекты, которые могут быть (1) запрошены для завершения, и (2) имеют продолжение, подписанное до их завершения. Компилятор генерирует код, который определяет, завершены ли возвращенные ожидания, а если нет, подписывает оставшуюся часть метода как завершение ожидаемого.
В частности, знает ли он, что первый из них связан с I/O
Неа. Он знает, что он возвращает что-то ожидаемое.
поэтому он должен использовать класс TaskCompletionSource, чтобы не было близости между потоком и задачей
Если вы заботитесь о деталях логики завершения, вы несете ответственность за то, чтобы контекст, в котором происходит ожидание, является правильным контекстом. Если вам все равно, вы получите соответствующий контекст по умолчанию.
а для второго он может использовать любой из методов, таких как Run или StartNew или Start, чтобы запланировать задачу в потоке пула потоков?
Компилятор не делает ничего подобного. Компилятор генерирует код, который проверяет, завершено ли возвращенное ожидание, а если нет, подписывает завершение ожидаемого. Как эти ожидаемые работы отвечают за вызов, а не за компилятор!
Ответ 3
Ни компилятор, ни среда выполнения не знают об этом. Фактически весь термин "связанный с IO" определен неопределенно. Является ли любая ОС вызовом IO?! Спящий или хронометр IO?!
Если вам нужно задать этот вопрос, у вас могут возникнуть некоторые недоразумения вокруг задач, потому что обычно это не нужно знать.
Может быть, распространенная ошибка думать, что ожидание запускает задачу? Он ждет завершения существующей задачи. DownloadImageDataAsync
и RenderAsync
решить, как и когда выполнить эту задачу. Поэтому они решают, использовать ли CPU или выполнять IO.
Когда DownloadImageDataAsync
и RenderAsync
передают вам задачу, которую вы не знаете, что внутри, и вам не нужно знать.