Почему асинхронная CTP работает плохо?

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

Хотя я был настроен скептически, я думал, что компилятор должен был переписать мой метод, чтобы загрузки выполнялись параллельно... но похоже, что на самом деле этого не происходит.
(я действительно понимаю что await и async не создают отдельных потоков; однако ОС должна выполнять загрузку параллельно и вызывать мой код в исходном потоке — не так ли?)

Я неправильно использую async и await? Как правильно их использовать?

Код:

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

static class Program
{
    static int SumPageSizesSync(string[] uris)
    {
        int total = 0;
        var wc = new WebClient();
        foreach (var uri in uris)
        {
            total += wc.DownloadData(uri).Length;
            Console.WriteLine("Received synchronized data...");
        }
        return total;
    }

    static async Task<int> SumPageSizesAsync(string[] uris)
    {
        int total = 0;
        var wc = new WebClient();
        foreach (var uri in uris)
        {
            var data = await wc.DownloadDataTaskAsync(uri);
            Console.WriteLine("Received async'd CTP data...");
            total += data.Length;
        }
        return total;
    }

    static int SumPageSizesManual(string[] uris)
    {
        int total = 0;
        int remaining = 0;
        foreach (var uri in uris)
        {
            Interlocked.Increment(ref remaining);
            var wc = new WebClient();
            wc.DownloadDataCompleted += (s, e) =>
            {
                Console.WriteLine("Received manually async data...");
                Interlocked.Add(ref total, e.Result.Length);
                Interlocked.Decrement(ref remaining);
            };
            wc.DownloadDataAsync(new Uri(uri));
        }
        while (remaining > 0) { Thread.Sleep(25); }
        return total;
    }

    static void Main(string[] args)
    {
        var uris = new string[]
        {
            // Just found a slow site, to demonstrate the problem :)
            "http://www.europeanchamber.com.cn/view/home",
            "http://www.europeanchamber.com.cn/view/home",
            "http://www.europeanchamber.com.cn/view/home",
            "http://www.europeanchamber.com.cn/view/home",
            "http://www.europeanchamber.com.cn/view/home",
        };
        {
            var start = Environment.TickCount;
            SumPageSizesSync(uris);
            Console.WriteLine("Synchronous: {0} milliseconds", Environment.TickCount - start);
        }
        {
            var start = Environment.TickCount;
            SumPageSizesManual(uris);
            Console.WriteLine("Manual: {0} milliseconds", Environment.TickCount - start);
        }
        {
            var start = Environment.TickCount;
            SumPageSizesAsync(uris).Wait();
            Console.WriteLine("Async CTP: {0} milliseconds", Environment.TickCount - start);
        }
    }
}

Выход:

Received synchronized data...
Received synchronized data...
Received synchronized data...
Received synchronized data...
Received synchronized data...
Synchronous: 14336 milliseconds
Received manually async data...
Received manually async data...
Received manually async data...
Received manually async data...
Received manually async data...
Manual: 8627 milliseconds          // Almost twice as fast...
Received async'd CTP data...
Received async'd CTP data...
Received async'd CTP data...
Received async'd CTP data...
Received async'd CTP data...
Async CTP: 13073 milliseconds      // Why so slow??

person user541686    schedule 15.01.2012    source источник


Ответы (2)


Ответ Криса почти правильный, но вводит состояние гонки и синхронно блокирует все задачи.

Как правило, лучше не использовать продолжения задач, если у вас есть await/async. Также не используйте WaitAny / WaitAll — эквивалентами async являются WhenAny и WhenAll.

Я бы написал так:

static async Task<int> SumPageSizesAsync(IEnumerable<string> uris)
{
  // Start one Task<byte[]> for each download.
  var tasks = uris.Select(uri => new WebClient().DownloadDataTaskAsync(uri));

  // Asynchronously wait for them all to complete.
  var results = await TaskEx.WhenAll(tasks);

  // Calculate the sum.
  return results.Sum(result => result.Length);
}
person Stephen Cleary    schedule 16.01.2012
comment
О да, это выглядит намного чище. =) Спасибо! - person user541686; 16.01.2012

Я могу неправильно прочитать ваш код, но похоже, что вы запускаете фоновый поток для выполнения асинхронного чтения, а затем сразу же блокируете, ожидая его завершения. Ничто в «асинхронной» части вашего кода на самом деле не является асинхронным. Попробуй это:

static async Task<int> SumPageSizesAsync(string[] uris)
{
    int total = 0;
    var wc = new WebClient();
    var tasks = new List<Task<byte[]>>();
    foreach (var uri in uris)
    {
        tasks
          .Add(wc.DownloadDataTaskAsync(uri).ContinueWith(() => { total += data.Length;
        }));
    }
    Task.WaitAll(tasks);
    return total;
}

И используйте его таким образом:

    {
        var start = Environment.TickCount;
        await SumPageSizesAsync(uris);
        Console.WriteLine("Async CTP: {0} milliseconds", Environment.TickCount - start);
    }

Я могу ошибаться - асинхронный материал новый, и я не знаком с ним на 100%, но время, аналогичное синхронной версии, кажется, подтверждает меня.

person Chris Shain    schedule 16.01.2012
comment
Хм... так что, думаю, возникает логичный вопрос: как правильно это сделать? - person user541686; 16.01.2012
comment
Отредактировано, чтобы добавить, как я думаю, что это должно работать. Опять же, может быть совершенно неправильно. - person Chris Shain; 16.01.2012
comment
Спасибо! Хотя я думал, что весь смысл await заключался в том, чтобы изменить остальную часть метода на стиль передачи продолжения (и весь смысл асинхронного CTP в том, чтобы устранить необходимость в таком большом количестве сантехники с делегатами/лямбдами)... так как это отличается от напр. BeginInvoke и еще много чего, что у нас уже было как минимум с .NET 2.0? - person user541686; 16.01.2012
comment
Это так, но в вашем случае ваше ожидание находится внутри цикла, поэтому цикл не может продолжаться до тех пор, пока оператор ожидания внутри него не вернется. По крайней мере, это мое чтение. Я не думаю, что оператор await обеспечивает параллелизм fork/join. - person Chris Shain; 16.01.2012
comment
Оооо, понятно... так что await приостанавливает весь метод и возвращается к вызывающей стороне, даже если части метода могут быть распараллелены? - person user541686; 16.01.2012
comment
Это мое понимание, да. Это не так умно, как вам хотелось бы. - person Chris Shain; 16.01.2012
comment
Хорошо, я думаю, это имеет смысл... но затем возникает вопрос: какое преимущество эта страница пытается продемонстрировать в первую очередь? Разве я не мог просто использовать синхронную версию? - person user541686; 16.01.2012
comment
В этом случае происходит одна загрузка, поэтому весь метод может быть приостановлен в этот момент до завершения DownloadTaskAsync, но в тот же момент Task‹int› возвращается вызывающей стороне. Вызывающий может заблокировать (или нет) эту задачу, в то время как загрузка продолжается в фоновом режиме. Итерация по списку строк URI и вызов этого метода для каждой из них завершится почти сразу, но приведет к множеству задач, которые вам придется ждать позже. - person Chris Shain; 16.01.2012
comment
Хм, хорошо, это просто оптимизация для всего набора загрузок, а не для отдельных загрузок... спасибо за объяснение! - person user541686; 16.01.2012