Прочитать на английском

Поделиться через


Внешние задачи и зерна

По задумке, все подзадачи, возникающие из кода зерна (например, с помощью await, ContinueWith или Task.Factory.StartNew) используют ту же единицу активации TaskScheduler, что и родительская задача. Таким образом, они наследуют ту же модель однопотокового выполнения, что и остальная часть зернового кода. Это основная точка выполнения однопотокового выполнения параллелизма на основе зерна.

В некоторых случаях коду зерна может потребоваться выйти из Orleans модели планирования задач и выполнить особые действия, например, конкретно указать Task на другой планировщик задач или .NET ThreadPool. Примером является ситуация, когда код 'grain' должен выполнить синхронный удалённый блокирующий вызов (например, удалённый ввод-вывод). Выполнение этого вызова в контексте зерна блокирует зерно и поэтому никогда не должно выполняться. Вместо этого код объекта grain может выполнить этот фрагмент блокирующего кода в потоке из пула потоков, дождаться завершения этого выполнения (await), а затем продолжить в контексте grain. Как ожидается, выход из Orleans планировщика будет очень сложным и редко необходимым сценарием использования, за пределами типичных шаблонов использования.

API на основе задач

  1. await, TaskFactory.StartNew (см. ниже), Task.ContinueWith, Task.WhenAny, Task.WhenAll, и Task.Delay все соответствуют текущему планировщику задач. Это означает, что они используются по умолчанию без передачи другого TaskScheduler, что приводит к их выполнению в контексте зерновой задачи.

  2. Оба Task.Run и endMethod делегат неTaskFactory.FromAsyncтекущий планировщик задач. Они оба используют диспетчер задач TaskScheduler.Default, диспетчер задач пула потоков .NET. Таким образом, код внутри Task.Run и endMethod, а также Task.Factory.FromAsyncвсегда выполняется в пуле потоков .NET, находясь вне модели однопоточного выполнения для зерен Orleans. Однако любой код после await Task.Run или await Task.Factory.FromAsync снова выполняется под управлением планировщика, который был активен в момент создания задачи, а именно планировщика grain.

  3. Task.ConfigureAwait с false помощью явного API для экранирования текущего планировщика задач. Это приводит к тому, что код после ожидания Task выполняется на TaskScheduler.Default планировщике (пул потоков .NET), что приводит к нарушению однопоточности работы зерна.

    Внимание!

    Как правило, никогда не используйте ConfigureAwait(false) непосредственно в коде зерна.

  4. Методы с сигнатурой async void не должны использоваться с зернами. Они предназначены для обработчиков событий графического пользовательского интерфейса. Метод async void может немедленно завершить текущий процесс, если позволяет исключению вырваться без возможности его обработки. Это также относится к List<T>.ForEach(async element => ...) и любому другому методу, принимающему Action<T>, так как асинхронный делегат преобразует в делегат async void.

Task.Factory.StartNew и async делегаты

Обычная рекомендация по планированию задач в C# используется Task.Run вместо Task.Factory.StartNew. Быстрый поиск Task.Factory.StartNew в Интернете показывает, что это опасно, и всегда рекомендуется отдавать предпочтение Task.Run. Тем не менее, чтобы оставаться в однопоточной модели выполнения контекста, Task.Factory.StartNew необходимо использовать. Итак, как правильно использовать его? Опасность Task.Factory.StartNew() заключается в отсутствии нативной поддержки асинхронных делегатов. Это означает, что код типа var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync), скорее всего, является ошибкой. notIntendedTask не является задачей, завершающейся после SomeDelegateAsync завершения. Вместо этого всегда распаковывает возвращаемую задачу: var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()

Пример. Несколько задач и планировщик задач

Ниже приведен пример кода, демонстрирующий использование TaskScheduler.Current, Task.Run, и специального настраиваемого планировщика, чтобы выйти из контекста Orleans зерна и как вернуться в него.

C#
public async Task MyGrainMethod()
{
    // Grab the grain's task scheduler
    var orleansTS = TaskScheduler.Current;
    await Task.Delay(10_000);

    // Current task scheduler did not change, the code after await is still running
    // in the same task scheduler.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);

    Task t1 = Task.Run(() =>
    {
        // This code runs on the thread pool scheduler, not on Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        Assert.AreEqual(TaskScheduler.Default, TaskScheduler.Current);
    });

    await t1;

    // We are back to the Orleans task scheduler.
    // Since await was executed in Orleans task scheduler context, we are now back
    // to that context.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);

    // Example of using Task.Factory.StartNew with a custom scheduler to escape from
    // the Orleans scheduler
    Task t2 = Task.Factory.StartNew(() =>
    {
        // This code runs on the MyCustomSchedulerThatIWroteMyself scheduler, not on
        // the Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        Assert.AreEqual(MyCustomSchedulerThatIWroteMyself, TaskScheduler.Current);
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    scheduler: MyCustomSchedulerThatIWroteMyself);

    await t2;

    // We are back to Orleans task scheduler.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);
}

Пример. Выполнение вызова зерн из кода, выполняемого в потоке пула потоков

Другой сценарий предусматривает необходимость кода зерна "выйти за пределы" модели планирования задач зерна и работать в потоке из пула потоков (или в другом контексте, отличном от зерна), при этом всё равно требуется вызвать другое зерно. Вызовы зерна можно выполнять из незерновых контекстов без дополнительной церемонии.

Следующий код демонстрирует выполнение вызова зерна из кода, выполняемого внутри зерна, но не в контексте зерна.

C#
public async Task MyGrainMethod()
{
    // Grab the Orleans task scheduler
    var orleansTS = TaskScheduler.Current;
    var fooGrain = this.GrainFactory.GetGrain<IFooGrain>(0);
    Task<int> t1 = Task.Run(async () =>
    {
        // This code runs on the thread pool scheduler,
        // not on Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        int res = await fooGrain.MakeGrainCall();

        // This code continues on the thread pool scheduler,
        // not on the Orleans task scheduler
        Assert.AreNotEqual(orleansTS, TaskScheduler.Current);
        return res;
    });

    int result = await t1;

    // We are back to the Orleans task scheduler.
    // Since await was executed in the Orleans task scheduler context,
    // we are now back to that context.
    Assert.AreEqual(orleansTS, TaskScheduler.Current);
}

Работа с библиотеками

Некоторые внешние библиотеки, используемые кодом, могут использовать ConfigureAwait(false) внутренне. Использование ConfigureAwait(false) является хорошей и правильной практикой в .NET при реализации библиотек общего назначения. Это не проблема в Orleans. Если код зерна, вызывающий метод библиотеки, ожидает вызова библиотеки с помощью регулярного await, то код зерна правильный. Результат точно такой, как нужно: код библиотеки запускает продолжения на планировщике по умолчанию (значение возвращаемое TaskScheduler.Default, которое не гарантирует выполнение продолжений в потоке ThreadPool, поскольку они часто встраиваются в предыдущий поток), в то время как код зерна запускается на планировщике зерна.

Другой часто задаваемый вопрос заключается в том, нужно ли выполнять вызовы библиотеки с Task.Run—то есть, требуется ли явная выгрузка библиотечного кода на ThreadPool (например, await Task.Run(() => myLibrary.FooAsync())). Ответ — нет. Разгружать код на ThreadPool не нужно, за исключением случаев, когда код библиотеки делает блокирующие синхронные вызовы. Как правило, любая хорошо написанная и правильная асинхронная библиотека .NET (методы, возвращающие Task и именованные с суффиксом Async ), не делают блокирующие вызовы. Таким образом, разгружать что-либо на ThreadPool не требуется, если не подозревается, что асинхронная библиотека содержит ошибки, или намеренно используется синхронная блокирующая библиотека.

Взаимоблокировки

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

  • Task.Wait()
  • Task.Result
  • Task.WaitAny(...)
  • Task.WaitAll(...)
  • task.GetAwaiter().GetResult()

Избегайте этих методов в любых системах с высокой степенью параллелизма, поскольку они могут привести к низкой производительности и нестабильности. Они истощают .NET ThreadPool, блокируя потоки, которые могли бы выполнять полезную работу, и требуют от ThreadPool внедрения дополнительных потоков для завершения. При выполнении кода зерна эти методы могут привести к взаимоблокировке, поэтому избегайте использовать их в коде зерна.

Если некоторую работу синхронизации поверх асинхронного избежать невозможно, лучше всего переместить её в отдельный планировщик. Самый простой способ — это использовать await Task.Run(() => task.Wait()), например. Обратите внимание, что настоятельно рекомендуется избегать синхронной работы , так как это вредит масштабируемости и производительности приложений.

Резюме: работа с задачами в Orleans

Какое действие вы пытаетесь выполнить? Как это сделать
Запустите фоновую работу с потоками пула потоков .NET. Не разрешены вызовы кода зерна или зерна. Task.Run
Выполните асинхронную рабочую задачу из кода зерна с Orleans гарантиями параллелизма на основе поворота (см. выше). Task.Factory.StartNew(WorkerAsync).Unwrap() (Unwrap)
Запустите синхронную рабочую задачу из кода зерна с Orleans гарантиями параллелизма на основе поворота. Task.Factory.StartNew(WorkerSync)
Время ожидания для выполнения рабочих элементов Task.Delay + Task.WhenAny
Вызов асинхронного метода библиотеки await Вызов библиотеки
Использование async/await Обычная модель программирования .NET Task-Async. Поддерживаемые и рекомендуемые
ConfigureAwait(false) Не используйте внутри кода зерна. Разрешено только в библиотеках.

Дополнительные ресурсы

Обучение

Модуль

Реализация асинхронных задач - Training

Узнайте, как реализовать асинхронные задачи в приложениях C# с помощью ключевых слов async и await и параллельного выполнения асинхронных задач.

События

.NET Conf 2025

4 нояб., 22 - 4 нояб., 22

.NET 10 запускается на .NET Conf 2025! Присоединяйтесь к сообществу .NET, чтобы отпраздновать и узнать о новом выпуске 11–13 ноября.

Сохраните дату