TaskEx.Yield(TaskScheduler)

В прошлом месяце я задал следующий вопрос, в результате которого я узнал о TaskEx.Yield:

Могут ли методы async иметь дорогой код перед первым "ожиданием" ?

Однако с тех пор я понял, что этот метод фактически передает весь последующий код в ambient TaskScheduler. В истинном духе DI наша команда согласилась избегать использования внешних экземпляров там, где это возможно, поэтому я хотел бы знать, можно ли явно указать TaskScheduler для использования?

Что-то вроде следующего было бы замечательно:

public static YieldAwaitable Yield(TaskScheduler taskScheduler)
{
    return new YieldAwaitable(taskScheduler);
}

Однако текущая реализация Async CTP предлагает только:

public static YieldAwaitable Yield()
{
    return new YieldAwaitable(SynchronizationContext.Current ?? TaskScheduler.Current);
}

Можно ли обеспечить приемлемую эффективную альтернативу?

await Task.Factory.StartNew(() => { }, CancellationToken.None, TaskCreationOptions.None, this.TaskScheduler);

Ответ 1

В истинном духе DI наша команда согласилась избегать использования внешних экземпляров, где это возможно...

Поддержка асинхронного языка основана на неявном контексте планирования. Я не вижу потребности в инъекции зависимостей здесь. Любой метод, вызывающий ваш метод async, может при необходимости предоставлять свой собственный контекст.

Ваша альтернатива:

await Task.Factory.StartNew(() => { }, CancellationToken.None, TaskCreationOptions.None, this.TaskScheduler);

не будет работать должным образом. Это будет помещать noop lambda в конкретный TaskScheduler, а затем возобновить метод в неявном контексте планирования.

Более ранняя версия Async CTP предоставила метод "yield to another context", называемый SwitchTo. Это было удалено, потому что это слишком легко использовать.

Лично я считаю, что чище держать ваш код async, используя его неявный контекст планирования, который предоставляется его вызывающим методом.

P.S. Не сложно создать и установить свой собственный контекст, например, для целей тестирования. Я написал AsyncContext в качестве простого контекста планирования для модульного тестирования и консольных программ. Async CTP поставляется с GeneralThreadAffineContext, WindowsFormsContext и WpfContext для тестирования. Любой из них может быть установлен с помощью SynchronizationContext.SetSynchronizationContext. IMO, DI является излишним.

Ответ 2

Языковая поддержка async позволяет awaitables управлять своим собственным расписанием. Существует планирование по умолчанию, которое срабатывает, когда вы ждете задание, но есть много способов изменить поведение по умолчанию с помощью немного большего количества кода.

В частности, ожидание предназначено для выполнения следующих действий:

  • Тест, чтобы проверить, выполнено ли "ожидание" (GetAwaiter(). IsCompleted).
  • Если (и только если) ожидаемое не было сделано, попросите его запланировать остальную часть метода (GetAwaiter(). OnCompleted (...))
  • "Реализуйте" ожидаемый результат. Это означает либо вернуть возвращаемое значение, либо гарантировать, что возникшие исключения будут возвращены.

Так что имейте в виду, что если ожидаемый утверждал, что это было сделано, тогда ничего не запланировано.

Task.Yield() является новым, потому что он возвращает ожидаемый результат никогда, с явной целью дать вам способ явно остановить выполнение на данный момент и немедленно запланировать остаток для выполнения. Он использует окружающий контекст, но есть много других способов сделать это без окружающих контекстов.

Примером переопределения поведения по умолчанию является то, что вы ожидаете неполную задачу, но используя метод ConfigureAwait (false). ConfigureAwait (false) завершает задачу в специальном ожидании, что всегда использует планировщик заданий по умолчанию, эффективно возобновляя поток в пуле потоков. "false" - это явно игнорировать контекст внешней синхронизации.

Не существует Task.Yield(). ConfigureAwait (false), но рассмотрим следующую гипотезу:

// ... A ...
await Task.Yield().ConfigureAwait(false);
// ... B ...

Вышеупомянутое может быть достигнуто в основном

// ... A ...
await Task.Run(() => {
    // ... B ...
});

Есть немного более очевидная и гнездовая там, но это не обязательно плохо, учитывая, что происходит. Часть "A" всегда работает на вызывающем потоке, тогда как часть "B" всегда работает в пуле потоков. Существуют определенные различия в том, как вы должны смотреть на код, который находится в разделах A и B, поэтому, таким образом, имея еще несколько завитушек между ними, мы надеемся заставить людей остановиться, прежде чем предполагать, что оба раздела являются одним и тем же контекстом.