Is it true that async should not be used for high-CPU tasks?

后端 未结 4 657
梦如初夏
梦如初夏 2021-01-31 04:11

I was wondering whether it\'s true that async-await should not be used for \"high-CPU\" tasks. I saw this claimed in a presentation.

So I guess

4条回答
  •  陌清茗
    陌清茗 (楼主)
    2021-01-31 04:56

    Let's say your CalculateMillionthPrimeNumber was something like the following (not very efficient or ideal in its use of goto but very simple to undertand):

    public int CalculateMillionthPrimeNumber()
    {
        List primes = new List(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();
    }
    

    Now, there's not useful point here at which this can do something asynchronously. Let's make it a Task-returning method using async:

    public async Task CalculateMillionthPrimeNumberAsync()
    {
        List primes = new List(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();
    }
    

    The compiler will warn us about that, because there's nowhere for us to await anything useful. Really calling this is going to be the same as a slightly more complicated version of calling Task.FromResult(CalculateMillionthPrimeNumber()). That is to say, it's the same as doing the calculation and then creating an already-completed task that has the calculated number as its result.

    Now, already-completed tasks aren't always pointless. For example, consider:

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

    This returns an already-completed task when the string is in the cache, and not otherwise, and in that case it will return pretty fast. Other cases are if there is more than one implementation of the same interface and not all implementations can use async I/O.

    And likewise an async method that awaits this method will return an already-completed task or not depending on this. It's actually a pretty great way of just staying on the same thread and doing what needs done when that is possible.

    But if it's always possible then the only effect is an extra bit of bloat around creating the Task object and the state-machine that async uses to implement it.

    So, pretty pointless. If that was how the version in your question was implemented then calculateMillionthPrimeNumber would have had IsCompleted returning true right from the beginning. You should have just called the non-async version.

    Okay, as the implementers of CalculateMillionthPrimeNumberAsync() we want to do something more useful for our users. So we do:

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

    Okay, now we're not wasting our user's time. DoIndependentWork() will do stuff at the same time as CalculateMillionthPrimeNumberAsync(), and if it it finishes first then the await will release that thread.

    Great!

    Only, we haven't really moved the needle that much from the synchronous position. Indeed, especially if DoIndependentWork() isn't arduous we may have made it a lot worse. The synchronous way would do everything on one thread, lets call it Thread A. The new way does the calculation on Thread B then either releases Thread A, then synchronises back in a few possible ways. It's a lot of work, has it gained anything?

    Well maybe, but the author of CalculateMillionthPrimeNumberAsync() can't know that, because the factors that influence that are all in the calling code. The calling code could have done StartNew itself, and been better able to fit the synchronisation options to the need when it did so.

    So, while tasks can be a convenient way of calling cpu-bound code in parallel to another task, methods that do so are not useful. Worse they're deceiving as someone seeing CalculateMillionthPrimeNumberAsync could be forgiven for believing that calling it wasn't pointless.

提交回复
热议问题