WebClient.DownloadDataCompleted не срабатывает

У меня очень странная проблема. Мой WebClient.DownloadDataCompleted большую часть времени не срабатывает.

Я использую этот класс:

public class ParallelFilesDownloader
{
    public Task DownloadFilesAsync(IEnumerable<Tuple<Uri, Stream>> files, CancellationToken cancellationToken)
    {
        var localFiles = files.ToArray();
        var tcs = new TaskCompletionSource<object>();
        var clients = new List<WebClient>();

        cancellationToken.Register(
            () =>
            {
                // Break point here
                foreach (var wc in clients.Where(x => x != null))
                    wc.CancelAsync();
            });

        var syncRoot = new object();
        var count = 0;
        foreach (var file in localFiles)
        {
            var client = new WebClient();

            client.DownloadDataCompleted += (s, args) =>
            {
                // Break point here
                if (args.Cancelled)
                    tcs.TrySetCanceled();
                else if (args.Error != null)
                    tcs.TrySetException(args.Error);
                else
                {
                    var stream = (Stream)args.UserState;
                    stream.Write(args.Result, 0, args.Result.Length);
                    lock (syncRoot)
                    {
                        count++;
                        if (count == localFiles.Length)
                            tcs.TrySetResult(null);
                    }
                }
            };
            clients.Add(client);

            client.DownloadDataAsync(file.Item1, file.Item2);
        }

        return tcs.Task;
    }
}

И когда я вызываю DownloadFilesAsync в LINQPad изолированно, DownloadDataCompleted вызывается примерно через полсекунды, как и ожидалось.

Однако в моем реальном приложении он просто не срабатывает, а код, ожидающий его завершения, просто зависает. У меня есть две точки останова, как указано в комментариях. Ни один из них не поражен.
Ах, но иногда он стреляет. Тот же URL, тот же код, просто новый сеанс отладки. Нет шаблона вообще.

Я проверил доступные потоки из пула потоков: workerThreads > 30k, completePortThreads = 999.

Я добавил сон на 10 секунд перед возвратом и проверил после сна, что мои веб-клиенты не были собраны мусором и что мой обработчик событий все еще подключен.

Теперь у меня закончились идеи, как решить эту проблему.
Что еще может быть причиной такого странного поведения?


person Daniel Hilgarth    schedule 01.09.2014    source источник
comment
Я собирался сказать, что с приведенным выше кодом clients выйдет за рамки и, вероятно, будет поражен сборщиком мусора. Однако вы указали, что когда вы спите в методе, clients, очевидно, все еще рядом. Если вы добавите сон, загрузка будет работать? Или вы все еще получаете такое же поведение? Кроме того, как долго вы ожидаете, что все загрузки займут?   -  person steve cook    schedule 01.09.2014
comment
у вас такое же поведение, когда вы запускаете свой проект в VS без подключенного отладчика?   -  person Eren Ersönmez    schedule 01.09.2014
comment
@steve: добавление сна не меняет поведение, загрузка по-прежнему не работает. Загрузка занимает меньше секунды, а URL-адрес указан для локального хоста, поэтому о проблемах с подключением также не может быть и речи. И clients не выходит из области действия из-за действия, переданного cancellationToken.Register.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
@ErenErsönmez: Да. Вот как я заметил, что что-то не так - приложение просто зависло.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
Надеюсь, вы не позвоните как DownloadFilesAsync(......).Wait()   -  person L.B    schedule 01.09.2014
comment
@LB Где-то позже есть Task.WaitAll, который ждет этой и других задач. Однако (1) я не понимаю, почему это повлияет на асинхронную загрузку - пожалуйста, уточните - и (2) проблема не исчезнет, ​​когда я добавлю сон, и поэтому Task.WaitAll не будет вызываться.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
@DanielHilgarth Попробуйте вызвать его с помощью await (await Task.WhenAll), не блокируя вызывающий поток (я подозреваю, что из-за тупика).   -  person L.B    schedule 01.09.2014
comment
Вы пытались перенести это в небольшой полный пример? Что-то еще в вашем коде делает что-то странное? Как вы сказали, если приведенный выше код отлично работает в другой среде, он может указывать на что-то еще.   -  person steve cook    schedule 01.09.2014
comment
@steve: Нет, я этого не делал, потому что у меня еще не было времени, которое было бы необходимо для этого.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
@DanielHilgarth Какое у вас производственное приложение?   -  person Yuval Itzchakov    schedule 01.09.2014
comment
@DanielHilgarth Небольшой пример, показывающий, что блокирующий вызов асинхронного метода может привести к взаимоблокировке var html = new WebClient().DownloadStringTaskAsync("http://stackoverflow.com").Result; (приложение Winforms)   -  person L.B    schedule 01.09.2014
comment
@LB: Спасибо за эту информацию. Я не знал об этом, но я не уверен, действительно ли это проблема: я не использую DownloadStringTaskAsync или DownloadDataTaskAsync. Я использую вариант, который не возвращает задачу. Задача, которую я возвращаю из своего метода, — это задача TaskCompletionSource, не связанная с WebClient.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
@DanielHilgarth, если DownloadDataCompleted запускается в контексте синхронизации вызывающего потока, то у вас та же проблема.   -  person L.B    schedule 01.09.2014
comment
@LB ... да, вполне может быть так. Позвольте мне проверить это.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
@DanielHilgarth Я проверил это и могу воспроизвести вашу проблему. Это не будет ответом, но вы можете опубликовать его, если хотите.   -  person L.B    schedule 01.09.2014
comment
@LB: Так почему я не могу воспроизвести это в LINQPad? У меня даже есть Task.WaitAll.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
@DanielHilgarth Скорее всего, вы также не сможете воспроизвести его в консольном приложении. Вам нужен Sync-Context, как в приложении winforms.   -  person L.B    schedule 01.09.2014
comment
@LB: Хорошо, это имеет смысл. Итак, какое решение вы предлагаете для .NET 4? нет ожидания, нет Task.WhenAll. Должен ли я эмулировать Task.WhenAll с потоком, который я запускаю сам? Я пытался использовать Task.Factory.ContinueWhenAll, но это не решило проблему. Я убедился, что в моем коде не осталось Task.WaitAll.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
@DanielHilgarth Не использовать асинхронность, создать задачу и использовать методы синхронизации :(   -  person L.B    schedule 01.09.2014
comment
@LB Не уверен, что ты имеешь в виду. Проблема в том, что DownloadDataCompleted не вызывается. ContinueWith на что? Обратите внимание, я не использую DownloadDataTaskAsync. Я использую DownloadDataAsync. Он возвращает void, а не Task.   -  person Daniel Hilgarth    schedule 01.09.2014
comment
@L.B: Да, прямо сейчас мой обходной путь — это поток, использующий методы синхронизации:/   -  person Daniel Hilgarth    schedule 01.09.2014


Ответы (2)


Из комментариев:

Где-то позже есть Task.WaitAll, который ждет эту и другие задачи. Однако (1) я не понимаю, почему это повлияет на асинхронную загрузку - пожалуйста, уточните - и (2) проблема не исчезнет, ​​когда я добавлю сон, и поэтому Task.WaitAll не будет вызываться

Похоже, у вас возникла тупиковая ситуация, вызванная Task.WaitAll. Я могу подробно объяснить это здесь:

Когда вы await используете асинхронный метод, который возвращает Task или Task<T>, происходит неявный захват SynchronizationContext с помощью TaskAwaitable, сгенерированного методом Task.GetAwaiter.

Как только этот контекст синхронизации создан и вызов асинхронного метода завершается, TaskAwaitable пытается маршалировать продолжение (которое, по сути, представляет собой вызовы остальных методов после первого ключевого слова await) в SynchronizationContext (с использованием SynchronizationContext.Post), которое было ранее захвачено. Если вызывающий поток заблокирован и ожидает завершения того же метода, возникает тупиковая ситуация.

Когда вы вызываете Task.WaitAll, вы блокируете до тех пор, пока все задачи не будут завершены, это сделает невозможным возврат к исходному контексту и, по сути, приведет к тупику.

Вместо Task.WaitAll используйте await Task.WhenAll.

person Yuval Itzchakov    schedule 01.09.2014
comment
OP отмечает, что даже WebClient.DownloadDataCompleted не срабатывает, который работает в пуле потоков и не участвует await. - person Eren Ersönmez; 01.09.2014
comment
@ErenErsönmez Точно. Я не использую методы из WebClient, которые возвращают задачу! - person Daniel Hilgarth; 01.09.2014
comment
Но вы ожидаете Task возврата из tcs - person Yuval Itzchakov; 01.09.2014
comment
@DanielHilgarth Я знаю, что вы не используете перегрузку, которая возвращает Task. Но ваше событие OnCompleted, вероятно, запускается в том же контексте синхронизации и вызывает этот тупик. Вы сказали, что используете .NET 4.0, я предлагаю вам установить Microsoft.Bcl.Async, чтобы получить функцию async-await. - person Yuval Itzchakov; 01.09.2014

Судя по комментариям, не идеальный ответ, но вы можете временно изменить контекст синхронизации до и после foreach:

var syncContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);

foreach (var file in localFiles)
{
    ...
}

SynchronizationContext.SetSynchronizationContext(syncContext);
person Eren Ersönmez    schedule 01.09.2014