Подтвердить что ты не робот

Верно ли, что async не должен использоваться для задач с высоким процессором?

Мне было интересно, правда ли, что async - await не следует использовать для задач с высоким процессором. Я видел, как это утверждалось в презентации.

Итак, я думаю, это означало бы что-то вроде

Task<int> calculateMillionthPrimeNumber = CalculateMillionthPrimeNumberAsync();
DoIndependentWork();
int p = await calculateMillionthPrimeNumber;

Мой вопрос может выше быть оправданным, или если нет, есть ли другой пример выполнения задачи с высоким процессором async?

4b9b3361

Ответ 1

Мне было интересно, правда ли, что async-await не следует использовать для задач с высоким процессором.

Да, это правда.

Мой вопрос: может ли это быть оправданным

Я бы сказал, что это не оправдано. В общем случае вам следует избегать использования Task.Run для реализации методов с асинхронными сигнатурами. Не выставляйте асинхронные обертки для синхронных методов. Это делается для предотвращения путаницы со стороны потребителей, особенно на ASP.NET.

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

var task = Task.Run(() => CalculateMillionthPrimeNumber());
DoIndependentWork();
var prime = await task;

Ответ 2

Существуют, по сути, два основных вида использования async/await. Один (и я понимаю, что это одна из основных причин, положенных в рамки) заключается в том, чтобы позволить вызывающему потоку выполнять другую работу во время ожидания результата. Это главным образом для задач с привязкой к I/O (т.е. задачи, в которых основной "holdup" - это какой-то ввод-вывод - ожидание того, что жесткий диск, сервер, принтер и т.д. Отвечает или выполняет свою задачу).

В качестве побочного примечания, если вы используете async/await таким образом, важно убедиться, что вы внедрили его таким образом, чтобы вызывающий поток мог фактически выполнять другую работу, пока он ждет результата; Я видел много случаев, когда люди делали такие вещи, как "A ждет B, который ждет C"; это может закончиться тем, что не работает лучше, чем если бы A просто называл B синхронно, а B просто называл C синхронно (потому что вызывающий поток никогда не позволял выполнять другую работу, ожидая результатов B и C).

В случае задач, связанных с I/O-привязкой, мало смысла создавать дополнительный поток, чтобы ждать результата. Моя обычная аналогия здесь состоит в том, чтобы думать о заказе в ресторане с 10 людьми в группе. Если первый человек, которого просит официант, еще не готов, официант не просто ждет, когда он будет готов, прежде чем он возьмет кого-то еще, и не принесет второго официанта, чтобы дождаться первого парня. Лучшее, что нужно сделать в этом случае, - попросить остальных 9 человек в группе по их приказам; надеюсь, к тому времени, когда они приказали, первый парень будет готов. Если нет, по крайней мере, официант все же сэкономил некоторое время, потому что он тратит меньше времени на простоя.

Также возможно использовать такие вещи, как Task.Run для выполнения задач, связанных с CPU (и это второе использование для этого). Чтобы следовать нашей аналогии выше, это случай, когда было бы полезно иметь больше официантов - например, если было слишком много таблиц для обслуживания одного официанта. Действительно, все, что на самом деле делает "за кулисами", это использование пула потоков; это одна из нескольких возможных конструкций для работы с привязкой к ЦП (например, просто поместив ее "прямо" в пул потоков, явно создав новый поток или используя Фоновый работник), так что это вопрос дизайна, в котором вы в конечном итоге используете.

Одно из преимуществ async/await заключается в том, что он (при правильных обстоятельствах) уменьшает количество явной логики блокировки/синхронизации, которую вы должны писать вручную. Вот своего рода немой пример:

private static async Task SomeCPUBoundTask()
    {
        // Insert actual CPU-bound task here using Task.Run
        await Task.Delay(100);
    }

    public static async Task QueueCPUBoundTasks()
    {
        List<Task> tasks = new List<Task>();

        // Queue up however many CPU-bound tasks you want
        for (int i = 0; i < 10; i++)
        {
            // We could just call Task.Run(...) directly here
            Task task = SomeCPUBoundTask();

            tasks.Add(task);
        }

        // Wait for all of them to complete
        // Note that I don't have to write any explicit locking logic here,
        // I just tell the framework to wait for all of them to complete
        await Task.WhenAll(tasks);
    }

Очевидно, я предполагаю, что задачи полностью распараллеливаются. Обратите также внимание на то, что вы могли бы использовать пул потоков непосредственно здесь, но это было бы немного менее удобно, потому что вам понадобился бы какой-то способ выяснить, все ли они завершены (а не просто позволить структуре оценить это для тебя). Вы также можете использовать здесь цикл Parallel.For.

Ответ 3

Скажем, ваш CalculateMillionthPrimeNumber был чем-то вроде следующего (не очень эффективный или идеальный в использовании goto, но очень простой для выполнения):

public int CalculateMillionthPrimeNumber()
{
    List<int> primes = new List<int>(1000000){2};
    int num = 3;
    while(primes.Count < 1000000)
    {
        foreach(int div in primes)
        {
            if ((num / div) * div == num)
                goto next;
        }
        primes.Add(num);
        next:
            ++num;
    }
    return primes.Last();
}

Теперь нет полезной точки, в которой это может сделать что-то асинхронно. Пусть это будет метод, возвращающий задачу, используя async:

public async Task<int> CalculateMillionthPrimeNumberAsync()
{
    List<int> primes = new List<int>(1000000){2};
    int num = 3;
    while(primes.Count < 1000000)
    {
        foreach(int div in primes)
        {
            if ((num / div) * div == num)
                goto next;
        }
        primes.Add(num);
        next:
            ++num;
    }
    return primes.Last();
}

Компилятор предупредит нас об этом, потому что нам нечего await ничего полезного. На самом деле вызов этого будет таким же, как немного более сложная версия вызова Task.FromResult(CalculateMillionthPrimeNumber()). То есть, так же, как и вычисление, и , а затем, создавая уже выполненную задачу, в результате которой рассчитывается число.

Теперь уже выполненные задачи не всегда бессмысленны. Например, рассмотрим:

public async Task<string> GetInterestingStringAsync()
{
    if (_cachedInterestingString == null)
      _cachedInterestingString = await GetStringFromWeb();
    return _cachedInterestingString;
}

Это возвращает уже выполненную задачу, когда строка находится в кеше, а не иначе, и в этом случае она вернется довольно быстро. Другими случаями являются случаи, когда существует более одной реализации одного и того же интерфейса, и не все реализации могут использовать async I/O.

А также метод async, который await этот метод вернет уже выполненную задачу или не будет зависеть от этого. Это на самом деле довольно отличный способ просто оставаться в одной теме и делать то, что нужно сделать, когда это возможно.

Но если это возможно всегда, единственным эффектом является дополнительный бит раздувания вокруг создания объекта Task и машины состояния, которые async использует для ее реализации.

Итак, довольно бессмысленно. Если бы это было так, как была реализована версия в вашем вопросе, то CalculateMillionthPrimeNumber имел бы IsCompleted возврат истины с самого начала. Вы должны были просто назвать не-асинхронную версию.

Хорошо, как разработчики CalculateMillionthPrimeNumberAsync() мы хотим сделать что-то более полезное для наших пользователей. Итак:

public Task<int> CalculateMillionthPrimeNumberAsync()
{
    return Task.Factory.StartNew(CalculateMillionthPrimeNumber, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
}

Хорошо, теперь мы не тратим наше время. DoIndependentWork() будет делать материал одновременно с CalculateMillionthPrimeNumberAsync(), и если он закончит сначала, то await выпустит этот поток.

Отлично!

Только, мы не очень сильно перемещали иглу из синхронного положения. Действительно, особенно если DoIndependentWork() не является трудным, мы, возможно, сделали его намного хуже. Синхронный способ будет делать все на одном потоке, позволяет называть его Thread A. Новый способ вычисляет на Thread B, затем либо выпускает Thread A, а затем синхронизирует несколько возможных способов. Это много работы, она что-то получила?

Ну, может быть, но автор CalculateMillionthPrimeNumberAsync() не может этого знать, потому что факторы, которые влияют на это, находятся в коде вызова. Вызывающий код мог бы сделать сам StartNew и был лучше способен подгонять параметры синхронизации к необходимости, когда это было сделано.

Таким образом, хотя задачи могут быть удобным способом вызова cpu-связанного кода параллельно с другой задачей, методы, которые делают это, не являются полезными. Хуже того, они обманывают, так как кто-то, видящий CalculateMillionthPrimeNumberAsync, может быть прощен за то, что он считает, что назвать это не бессмысленно.

Ответ 4

Если CalculateMillionthPrimeNumberAsync постоянно использует async/await сам по себе, нет причин не позволять Задаче запускать тяжелую работу ЦП, поскольку он просто делегирует ваш метод потоку ThreadPool.

Что такое поток ThreadPool и как он отличается от обычного потока, написано здесь.

Короче говоря, он довольно долго занимает поток threadpool в кастодию (и количество потоков threadpool ограничено), поэтому, если вы не принимаете слишком много их, вам не о чем беспокоиться.