it-swarm.com.ru

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

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

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

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

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

22
user7127000

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

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

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

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

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

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

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

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

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

Также возможно использовать такие вещи, как Task.Run для выполнения задач, связанных с процессором (и это второе использование для этого). Чтобы следовать нашей аналогии, приведенной выше, это тот случай, когда в целом было бы полезно иметь больше официантов - например, если бы было слишком много столов для обслуживания одного официанта. На самом деле, все, что это на самом деле делает «за кулисами», - это использование пула потоков; это одна из нескольких возможных конструкций для выполнения работы с привязкой к процессору (например, просто «поставить» ее непосредственно в пул потоков, явно создать новый поток или использовать Background Worker ), так что это вопрос дизайна, какой механизм вы заканчиваете до использования.

Одним из преимуществ 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 здесь.

8
EJoshuaS

Допустим, ваш 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, который awaits этот метод будет возвращать уже выполненную задачу или нет в зависимости от этого. На самом деле это очень хороший способ просто оставаться в одном потоке и делать то, что нужно сделать, когда это возможно.

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

Так что, довольно бессмысленно. Если бы именно так была реализована версия в вашем вопросе, то calculateMillionthPrimeNumber имел бы IsCompleted, возвращающую true с самого начала. Вы должны были просто назвать не асинхронную версию.

Хорошо, как разработчики 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 и был бы лучше приспособлен к параметрам синхронизации, когда это необходимо.

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

4
Jon Hanna

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

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

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

1
AgentFire