Ребят, помогите плиз с этой %#$#^ асинхронностью и WPF?
Пишу такой велосипед, что удивляешься, почему он требует так много усилий!
Задача — "красивая" скачка файлов: есть список картинок, они ставятся в очередь и качаются, попутно отображая прогресс.
Очередь ограничена 5 работающими трэдами (хотя и без Семафора те же проблемы).
Как ДОЛЖНО работать: из 10 картинок 5 всегда должны отображать некий прогресс, пока все скачки не закончатся.
Что НЕ ТАК работает: ставлю 10 картинок, в UI — тишина. Ждём где-то 15 секунд и тут UI оживает: 7 скачек тут же отображаются как "готово", а из остальных только ДВЕ качаются (прогресс виден), третья ждёт, потом одна из двух завершается и третья тоже докачивается, причём у всех трёх прогресс был виден!
То есть имеем ряд проблем:
1. Качается только ДВА задания, хотя работающих трэдов — несколько. Такое ощущение, что приложению не дают юзать больше 2 сокетов Кто-то слышал про подобные ограничения?
2. UI не показывает изменения (хотя они есть), нормально начинает работать лишь спустя 15 секунд — что за фигня? Кого и где там ждут?
Сам код — думаю, вы легко разберётесь, везде комменты.
async void Download_Clicked(object sender, RoutedEventArgs e)
{
semDL = new Semaphore(5, 5);
var currTasks = new List<Task>();// здесь собираем все таски, чтобы потом их ждатьforeach (var pic in PicsToLoad) {
while (!semDL.WaitOne(500)) {// limit maximum downloading threads
DoEvents();// даже принудительное обновление UI не помогает!!
}
currTasks.Add(Task.Factory.StartNew(() => {
var web = new WebClient();// может, это WebClient такой уродский? Кто-то сталкивался с его некорректным поведением?
web.DownloadProgressChanged += Web_DownloadProgressChanged;// этот метод явно вызывается - проверено
web.DownloadFileCompleted += Web_DownloadFileCompleted;// этот тоже
web.DownloadFileTaskAsync(new Uri(pic), GeneratedName).Wait();// ждём завершения закачки и освобождаем семафор
semDL.Release();
}));
}
await Task.WhenAll(currTasks.ToArray());
}
public void DoEvents()
{
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, (Action)(() => { }));// подсмотрено в тырнетах
}
void Web_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
var url = (Uri)((TaskCompletionSource<object>)e.UserState).Task.AsyncState;
PicStatus = $"Loaded {e.BytesReceived:N0} / {e.TotalBytesToReceive:N0} ({e.ProgressPercentage}%)";
DoEvents();// это тоже не помогает!
}
Вот картинка с закачками, здесь Count = число обращений к Web_DownloadProgressChanged и +1,000,000 если было обращение к Web_DownloadFileCompleted. Как видно, этот костыль WebClient даже не удосуживается везде вызывать финальный коллбэк (скачки со счётчиками 13, 11, 7)! При этом видно, что каждая закачка обращалась в среднем около десятка раз и НИ РАЗУ не отобразилась в UI! (прогресс забайнден на грид и у картинки дёргается PropertyChanged)
Если бы вообще ничего не работало, я б винил алгоритм/неумение работать с async, но ведь работает!! Но не сразу Есть какие-нибудь идеи?
Здравствуйте, Kolesiki, Вы писали:
K>1. Качается только ДВА задания, хотя работающих трэдов — несколько. Такое ощущение, что приложению не дают юзать больше 2 сокетов Кто-то слышал про подобные ограничения?
ХА-ХА! Не думал, что нагуглю этот маразм, взлелеянный MoronSoftom!! Да, оказывается ЕСТЬ ОГРАНИЧЕНИЕ! Кто бы мог подумать, что заботливые %идёт непереводимая игра слов с использованием местных идиоматических выражений% из мудософта заделают глобальный параметр ServicePointManager.DefaultConnectionLimit! И что мало нам гемороя с сокетами, ты ещё найди их неизвестный ServicePointManager и поколдуй над ним — нам же мало проблем с кодом!
Нет слов, хочется с ноги взять и.... научить студоту не лезть туда, где даже у дедушки не лез!
Здравствуйте, Kolesiki, Вы писали:
K>Нет слов, хочется с ноги взять и.... научить студоту не лезть туда, где даже у дедушки не лез!
Кэп: если писать код методом копипасты с stackoverflow, то да, приходится играть в д'Артаньяна и Микрософт.
А если для разнообразия изучить матчасть, то и с блокировкой UI-потока не надо извращаться, и решение стандартное находится на раз-два, да и без ругани всё как-то работает, сам удивляюсь
Другими словами: сначала разобраться, потом обвинять в рукожопости. Порядок важен.
The HttpClient object is intended to live for as long as your application needs to make HTTP requests. Having an object exist across multiple requests enables a place for setting DefaultRequestHeaders and prevents you from having to respecify things like CredentialCache and CookieContainer on every request, as was necessary with HttpWebRequest.
Здравствуйте, Sinix, Вы писали:
S>Кэп: если писать код методом копипасты с stackoverflow, то да, приходится играть в д'Артаньяна и Микрософт.
Если проектировать через жопу — то да, приходится играть в бога и вводить ***ер знает кому нужные ServicePointManager — кто додумался до этого маразма и кто его просил вообще вводить?
S>А если для разнообразия изучить матчасть, то и с блокировкой UI-потока не надо извращаться, и решение стандартное
А! Тогда понятно, сразу бы и сказал: сначала Микрософт "копирует со стэковерфлова" класс Semaphore, затем использует, задумчиво чешет репу (ну, ту, которой они как бы думают), а затем реализуют ЕЩЁ ОДИН ДУБЛИРУЮЩИЙ КЛАСС под названием SemaphoreSlim и пишут 10-страничную "матчасть" (которую вы предлагаете непонятно где брать — с того же стэковерфлова), и тогда всё работает!
За труды конечно спасибо — буду знать, что *Slim придуман неспроста (и он мне уже попадался в доках), но согласитесь, что это ГРАБЛИ, а не "матчасть".
Ну и чтоб 2 раза не вставать, решение пришлось делать хирургическое:
С постоянным рефрешем скачки работают и плавно обновляются. Оказывается, просто Semaphore.Wait(таймаут) уходит НАГЛУХО в себя, не удосуживая даже дать кусочек дотнетине. Как так? Если мы чего-то ЖДЁМ, то время ожидания можно как раз легко потратить на остальные трэды (в частности, ГУЁвый). У WPF наблюдаем глушняк.
Здравствуйте, Sinix, Вы писали:
S>Другими словами: сначала разобраться, потом обвинять в рукожопости. Порядок важен.
Рукожопость — это про ServicePointManager — согласись, что человек, который и так тратит кучу времени на разборки в МС классах, не должен ещё лезть в какие-то сервисные дебри только потому, что кто-то мелкомягкий умом решил поиграть в ограничения для юзеров! (кстати, это не первый факап — у XP где-то в регистри тоже была чуча, которая не давала использовать больше 20 сокетов — пришлось опять хачить!)
Я не поленился, переделал на SemaphoreSlim — за счёт await throttler.WaitAsync() всё заработало, спасибо ещё раз!
Здравствуйте, _Raz_, Вы писали:
_R_>In genral I would recommend reusing HttpClient instances as much as possible.
Ну да, станартная фишка для всех реализаций HttpClient, я тут особой проблемы не вижу
Если сравнивать с реализацией в яве — я про чехарду с ThreadSafeClientConnManager->PoolingClientConnectionManager->PoolingHttpClientConnectionManager — так вообще рай и тишина.
Здравствуйте, Kolesiki, Вы писали:
K>Я не поленился, переделал на SemaphoreSlim — за счёт await throttler.WaitAsync() всё заработало, спасибо ещё раз!
Парень, без обид, но ты сейчас выглядишь как первокурсник, на перезачёте узнавший что "всю эту хрень надо было ещё и учить?".
Вэлкам в реальную жизнь
Не нравится — можешь попробовать насладиться творческими решениями конкурентов. Очень рекомендую начать с полёта фантазии в яве.
Для просветления, такскть
Здравствуйте, Kolesiki, Вы писали:
K>Сам код — думаю, вы легко разберётесь, везде комменты.
K>
K> var web = new WebClient();// может, это WebClient такой уродский? Кто-то сталкивался с его некорректным поведением?
K> web.DownloadProgressChanged += Web_DownloadProgressChanged;// этот метод явно вызывается - проверено
K> web.DownloadFileCompleted += Web_DownloadFileCompleted;// этот тоже
K> web.DownloadFileTaskAsync(new Uri(pic), GeneratedName).Wait();// ждём завершения закачки и освобождаем семафор
K>
У WebClient есть по три версии методова загрузки:
1. DownloadFile — синхронный,
2. DownloadFileAsync — асинхронный, работает вместе с событиями DownloadProgressChanged, DownloadFileCompleted,
3. DownloadFileTaskAsync — асинхронный, основан на тасках.
В приведённом коде я вижу вызов метода возвращающего Task, при этом подписка на события другого метода...
Здравствуйте, Sinix, Вы писали:
S>Парень, без обид, но ты сейчас выглядишь как первокурсник, на перезачёте узнавший что "всю эту хрень надо было ещё и учить?".
Да какие обиды на агентов микрософта!
То, как я выгляжу в их глазах, никак не отменяет факта, что помимо класса "сокет" юзер вообще НИЧЕГО не обязан знать дополнительно — почитай любой туториал на Линукс — там ВСЁ, что нужно для работы с тырнетом — это сокет. Расскажи мне, я уже в ТРЕТИЙ раз прошу, кому и за каким хреном понадобился класс ServicePointManager, который за 10(!) лет программирования сетей в дотнете я вижу впервые? (и кому взбрело в голову ставить ГЛОБАЛЬНЫЕ ограничения там, где их не просили)
Короче, понимаешь, это не я студент — это ваша контора — студиозы (причём биологические), у которых (классическая психология подростков) просто чешется комплекс бога — всё запретить, ограничить и т.п. Я вас понимаю, но не оправдываю такое похабное проектирование.
Придумать сами себе сто классов и назвать это фрэймворком — это ещё не повод тыкать профессионалов "вы недостаточно покопались в нашем навозе" — запомни эту мысль.
Здравствуйте, koodeer, Вы писали:
K>> web.DownloadProgressChanged += Web_DownloadProgressChanged;// этот метод явно вызывается — проверено K>> web.DownloadFileCompleted += Web_DownloadFileCompleted;// этот тоже K>> web.DownloadFileTaskAsync(new Uri(pic), GeneratedName).Wait();// ждём завершения закачки и освобождаем семафор
K>У WebClient есть по три версии методова загрузки: K>1. DownloadFile — синхронный, K>2. DownloadFileAsync — асинхронный, работает вместе с событиями DownloadProgressChanged, DownloadFileCompleted, K>3. DownloadFileTaskAsync — асинхронный, основан на тасках.
K>В приведённом коде я вижу вызов метода возвращающего Task, при этом подписка на события другого метода...
Ха... MSDN и я читал! И самый прикол в том, что DownloadFileTaskAsync тоже работает! (да это и вполне логично — ожидать от асинхроны репортов прогресса)
А без тасков как мне ждать завершения-то?
Здравствуйте, Kolesiki, Вы писали:
K> помимо класса "сокет" юзер вообще НИЧЕГО не обязан знать дополнительно K> не повод тыкать профессионалов "вы недостаточно покопались в нашем навозе" — запомни эту мысль.
Здравствуйте, Kolesiki, Вы писали:
K>Ха... MSDN и я читал! И самый прикол в том, что DownloadFileTaskAsync тоже работает! (да это и вполне логично — ожидать от асинхроны репортов прогресса) K>А без тасков как мне ждать завершения-то?
Здравствуйте, Kolesiki, Вы писали:
K>То, как я выгляжу в их глазах, никак не отменяет факта, что помимо класса "сокет" юзер вообще НИЧЕГО не обязан знать дополнительно — почитай любой туториал на Линукс — там ВСЁ, что нужно для работы с тырнетом — это сокет.
Ну юзай сокеты, проблем то
K>Расскажи мне, я уже в ТРЕТИЙ раз прошу, кому и за каким хреном понадобился класс ServicePointManager,
Да тебе же и понадобился. Что, не догадаться в своей задаче установить ConnectionLimit = 5?
K> который за 10(!) лет программирования сетей в дотнете я вижу впервые? (и кому взбрело в голову ставить ГЛОБАЛЬНЫЕ ограничения там, где их не просили)
There are a whole bunch of other things that you can take advantage of in ServicePointManager – things such as the HttpConnection limit, which is based upon a W3 spec, but for internal back-end service calls over REST and the like, you may want to affect.
И ограничения не глобальные. Можно настроить хоть для каждого инстанса WebClient.
Здравствуйте, Kolesiki, Вы писали:
K>Ребят, помогите плиз с этой %#$#^ асинхронностью и WPF? K>Пишу такой велосипед, что удивляешься, почему он требует так много усилий!
Это мироздание посылает тебе сигнал: ты что-то делаешь не так! Надо остановиться и подумать, всегда помогает. K>Задача — "красивая" скачка файлов: есть список картинок, они ставятся в очередь и качаются, попутно отображая прогресс. K>Очередь ограничена 5 работающими трэдами (хотя и без Семафора те же проблемы). K>Как ДОЛЖНО работать: из 10 картинок 5 всегда должны отображать некий прогресс, пока все скачки не закончатся.
При чём тут "трэды"? Насколько я понял, тебе нужно, чтобы в любой момент времени выполнялось не более 5 заданий на скачку. И вообще, скачиванием занимается сетевое железо, ей "трэды" не нужны. Когда железо получит порцию данных, оно дёрнет процессор, который до этого момента вообще занимается своими делами.
async void Download_Clicked(object sender, RoutedEventArgs e)
{
var doneJobCount = 0;
// Пока не выполнили все заданияwhile (doneJobCount < PicsToLoad.Count)
{
// Берём очередных 5 заданийvar jobs = PicsToLoad.Skip(doneJobCount).Take(5);
var jobTasks =
// Для каждого заданияfrom job in jobs
// создаём качалку, подписываемся на события, запоминаем переданное задание...let webClient = CreateWebClient(job)
// запускаем скачиваниеselect webClient.DownloadFileTaskAsync(job.Url, job.FileName);
// Качаем и ждём завершения
await Task.WhenAll(jobTasks);
// Подсчитываем выполненные задания
doneJobCount += jobs.Count();
}
}
Здравствуйте, Vladek, Вы писали:
K>>Очередь ограничена 5 работающими трэдами (хотя и без Семафора те же проблемы). K>>Как ДОЛЖНО работать: из 10 картинок 5 всегда должны отображать некий прогресс, пока все скачки не закончатся.
V>При чём тут "трэды"?
Привык я к ним. Мне легче понимать "параллельные задачи" именно как явно ощущаемый кусок кода. А надеяться на мелкомягкие "пулы" — спасиб, один раз понадеялся — на ДВУХСОКЕТНЫЙ ПУЛ, блин позорище, а не фрэймворк.
V> Насколько я понял, тебе нужно, чтобы в любой момент времени выполнялось не более 5 заданий на скачку.
Понял — правильно, но вот реализовал по-моему криво.
V> // Берём очередных 5 заданий V> var jobs = PicsToLoad.Skip(doneJobCount).Take(5); V> // Качаем и ждём завершения V> await Task.WhenAll(jobTasks);
НЕТ! Мы не качаем "блоками по 5 файлов", а держим постоянную очередь из 5 (максимум) работяг. Если хотя бы один закончил работу, то на его место приходит новое задание на закачку. А у тебя ждёт всех 5-ти, а потом снова берёт 5.
Здравствуйте, Kolesiki, Вы писали: K>НЕТ! Мы не качаем "блоками по 5 файлов", а держим постоянную очередь из 5 (максимум) работяг. Если хотя бы один закончил работу, то на его место приходит новое задание на закачку. А у тебя ждёт всех 5-ти, а потом снова берёт 5.
Тогда можно за отправную точку взять вот этот код:
Scheduler.cs
class ServiceRequestScheduler : IDisposable
{
private readonly ServiceJobFactory jobFactory;
private readonly ConcurrentQueue<ServiceJob> jobQueue;
private readonly Timer timer;
private readonly TimeSpan runPeriod;
private readonly int maxNumConcurrentJobs;
private int numConcurrentJobs;
public ServiceRequestScheduler(ServiceJobFactory jobFactory)
: this(2, TimeSpan.FromSeconds(30))
{
this.jobFactory = jobFactory;
}
private ServiceRequestScheduler(int numConcurrentJobs, TimeSpan runPeriod)
{
this.maxNumConcurrentJobs = numConcurrentJobs;
this.runPeriod = runPeriod;
this.jobQueue = new ConcurrentQueue<ServiceJob>();
this.timer = new Timer(RunJobs);
this.timer.Change(TimeSpan.Zero, this.runPeriod);
}
public Task Schedule(ServiceRequest request, CancellationToken cancelToken)
{
var job = this.jobFactory.CreateJob(request, cancelToken);
job.Started += HandleJobStarted;
job.Stopped += HandleJobStopped;
this.jobQueue.Enqueue(job);
return job.Task;
}
public void Dispose()
{
this.timer.Dispose();
}
private void HandleJobStopped(object sender, EventArgs e)
{
Interlocked.Decrement(ref this.numConcurrentJobs);
}
private void HandleJobStarted(object sender, EventArgs e)
{
Interlocked.Increment(ref this.numConcurrentJobs);
}
private void RunJobs(object state)
{
var numJobs = this.maxNumConcurrentJobs - this.numConcurrentJobs;
for (var i = 0; i < numJobs; ++i)
{
ServiceJob job;
if (this.jobQueue.TryDequeue(out job))
job.RunAsync();
}
}
}
abstract class ServiceJob
{
private readonly TaskCompletionSource<ServiceJob> taskSource;
protected ServiceJob()
{
this.taskSource = new TaskCompletionSource<ServiceJob>();
}
public Task Task { get { return this.taskSource.Task; } }
public event EventHandler Started;
public event EventHandler Stopped;
public async Task RunAsync()
{
try
{
OnStarted(EventArgs.Empty);
await RunOverrideAsync().ConfigureAwait(false);
this.taskSource.SetResult(this);
}
catch (OperationCanceledException)
{
this.taskSource.TrySetCanceled();
}
catch (Exception x)
{
this.taskSource.TrySetException(x);
}
finally
{
OnStopped(EventArgs.Empty);
}
}
protected virtual void OnStarted(EventArgs e)
{
var started = this.Started;
if (started != null)
{
started(this, e);
}
}
protected virtual void OnStopped(EventArgs e)
{
var stopped = this.Stopped;
if (stopped != null)
{
stopped(this, e);
}
}
protected abstract Task RunOverrideAsync();
}
Здравствуйте, Vladek, Вы писали:
V> // Берём очередных 5 заданий V> var jobs = PicsToLoad.Skip(doneJobCount).Take(5);
V> // Качаем и ждём завершения V> await Task.WhenAll(jobTasks);
Если из 5 файлов 4 маленьких, а 1 большой, то такое распределение (группами по 5) не эффективно, будет попусту теряться время.
Вообще меня смутило
K> foreach (var pic in PicsToLoad) { K> while (!semDL.WaitOne(500)) {// limit maximum downloading threads K> DoEvents();// даже принудительное обновление UI не помогает!! K> }
Зачем асинхронно в UI потоке ждать семафор (еще и под-извините-пердывать диспетчером)? Имхо, красивее будет обычный producer/consumer: некий клас, создаете инстанс, вызываете асинхронно await Work(IEnumerable<FileModel> files), который перегоняет IEnumerable в ConcurentQueue, запускает 5 (или сколько нужно) потоков/задач (не нужно семафоров), в каждой задаче берется из очереди итем (TryDequeue), скачивается (со всеми эвентами, типа DownloadStarted, DownloadProgress, DownloadFinished), когда все завершило работу — метод возвращает управление. Для каждого файла создаете FileViewModel, которая подписывается на события и, устанавливая различные свойста, управляет тем, как файл отображается во View. ObservableCollection<FileViewModel> биндиться к ItemSource какого-нибудь списка, у которого ItemTemplate динамический: используя дата триггеры, дата темплейты или просто через конвертер управляя Visibility.