Что происходит с Task.Delay (). Wait ()? - PullRequest
0 голосов
/ 06 декабря 2018

Я запутался, почему Task.Delay().Wait() занимает в 4 раза больше времени , тогда Thread.Sleep()?

Например, task-00 работал в только потоке 9 и занимал 2193ms ?Я знаю, что ожидание синхронизации плохо в задачах, потому что весь поток блокируется.Это просто для теста.

Простой тест в консольном приложении:

bool flag = true;
var sw = Stopwatch.StartNew();
for (int i = 0; i < 10; i++)
{
    var cntr = i;
    {
        var start = sw.ElapsedMilliseconds;
        var wait = flag ? 100 : 300;
        flag = !flag;

        Task.Run(() =>
        {
            Console.WriteLine($"task-{cntr.ToString("00")} \t ThrID: {Thread.CurrentThread.ManagedThreadId.ToString("00")},\t Wait={wait}ms, \t START: {start}ms");                     
            //Thread.Sleep(wait);
            Task.Delay(wait).Wait();
            Console.WriteLine($"task-{cntr.ToString("00")} \t ThrID: {Thread.CurrentThread.ManagedThreadId.ToString("00")},\t Wait={wait}ms, \t END: {sw.ElapsedMilliseconds}ms");
            ;
        });
    }
}
Console.ReadKey();
return;

С Task.Delay().Wait():
task-03 ThrID: 05, Wait =300 мс, запуск: 184 мс
Task-04 ThrID: 07, ожидание = 100 мс, START: 184 мс
task-00 ThrID: 09, ожидание = 100 мс, START: 0 мс
task-06 ThrID: 04, ожидание= 100 мс, START: 185 мс
Task-01 ThrID: 08, ожидание = 300 мс, START: 183 мс
task-05 ThrID: 03, ожидание = 300 мс, START: 185 мс
task-02 ThrID: 06,Ожидание = 100 мс, START: 184 мс
Task-07 ThrID: 10, ожидание = 300 мс, START: 209 мс
Task-07 ThrID: 10, ожидание = 300 мс, END: 1189 мс
task-08 ThrID: 12, Ожидание = 100 мс, START: 226 мс
task-09 ThrID: 10, ожидание = 300 мс, START: 226 мс
task-09 ThrID: 10, ожидание = 300 мс, END: 2192 мс
task-06 ThrID:04, Wait = 100 мс, END: 2193 мс
task-08 ThrID: 12, Wait = 100 мс, END: 2194 мс
task-05 ThrID: 03, Wait = 300 мс, END: 2193 мс
task-03 ThrID: 05, ожидание = 300 мс, END: 2193 мс
Task-00, ThrID: 09, ожидание = 100 мс, END: 2193 мс
Task-02, ThrID: 06, ожидание = 100 мс, END: 2193 мс
task-04 ThrID: 07, ожидание = 100 мс, END: 2193 мс
task-01 ThrID: 08, ожидание = 300 мс, END: 2193 мс

с Thread.Sleep():
Task-00 ThrID: 03, ожидание = 100 мс, START: 0 мс
task-03 ThrID: 09, ожидание = 300 мс, START: 179 мс
task-02 ThrID: 06, ожидание = 100 мс,START: 178ms
task-04 ThrID: 08, ожидание = 100 мс, START: 179ms
task-05 ThrID: 04, Wait = 300 мс, START: 179ms
task-06 ThrID: 07, ожидание = 100 мс, НАЧАЛО: 184 мс
Task-01 ThrID: 05, ожидание = 300 мс, START: 178 мс
Task-07 ThrID: 10, ожидание = 300 мс, START: 184 мс
Task-00 ThrID: 03, ожидание =100 мс, END: 284 мс
Task-08 ThrID: 03, ожидание = 100 мс, START: 184 мс
task-02 ThrID: 06, ожидание = 100 мс, END: 285 мс
task-09 ThrID: 06, ожидание= 300 мс, START: 184 мс
Task-04 ThrID: 08, ожидание = 100 мс, END: 286 мс
task-06 ThrID: 07, ожидание = 100 мс, END: 293 мс
task-08 ThrID: 03,Wait = 100 мс, END: 385 мс
Task-03 ThrID: 09, Wait = 300 мс, END: 485 мс
task-05 ThrID: 04, Wait = 300 мс, END: 486 мс
task-01 ThrID: 05, Втait = 300 мс, END: 493 мс
Task-07 ThrID: 10, ожидание = 300 мс, END: 494 мс
task-09 ThrID: 06, ожидание = 300 мс, END: 586 мс

Изменить:
С async лямбда и await Task.Delay() так же быстро, как Thread.Sleep(), может быть также быстрее (511 мс).
Редактировать 2:
С ThreadPool.SetMinThreads(16, 16); Task.Delay().Wait() работает так же быстро, как Thread.Sleep за 10 итераций в цикле.С большим количеством итераций это снова медленнее.Также интересно, что если без настройки я увеличу количество итераций для Thread.Sleep до 30 , это все равно будет быстрее, тогда 10 итерация с Task.Delay().Wait()
Изменить 3:
Перегрузка Task.Delay(wait).Wait(wait) работает так же быстро, как Thread.Sleep()

Ответы [ 3 ]

0 голосов
/ 12 декабря 2018

Ваша проблема в том, что вы смешиваете асинхронный код с синхронным кодом без использования async и await.Не используйте синхронный вызов .Wait, он блокирует ваш поток, и поэтому асинхронный код Task.Delay() не будет работать должным образом.

Асинхронный код часто не работает должным образом при синхронном вызове, потому что это не такпредназначен для работы таким образом.Вы можете получить удачу, и асинхронный код работает синхронно.Но если вы используете какую-то внешнюю библиотеку, автор этой библиотеки может изменить свой код таким образом, что это нарушит ваш код.Асинхронный код должен быть полностью асинхронным.

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

Ваш код должен выглядеть ниже, но с async Вы не можете быть уверены, что ManagedThreadId останется прежним.Потому что поток выполнения вашего кода может измениться во время выполнения.Никогда не следует использовать свойство ManagedThreadId или атрибут [ThreadStatic], если вы все равно используете асинхронный код по этой причине.

Асинхронное / ожидание - Рекомендации по асинхронному программированию

bool flag = true;
var sw = Stopwatch.StartNew();
for (int i = 0; i < 10; i++)
{
    var cntr = i;
    {
        var start = sw.ElapsedMilliseconds;
        var wait = flag ? 100 : 300;
        flag = !flag;

        Task.Run(async () =>
        {
            Console.WriteLine($"task-{cntr.ToString("00")} \t ThrID: {Thread.CurrentThread.ManagedThreadId.ToString("00")},\t Wait={wait}ms, \t START: {start}ms");
            await Task.Delay(wait);
            Console.WriteLine($"task-{cntr.ToString("00")} \t ThrID: {Thread.CurrentThread.ManagedThreadId.ToString("00")},\t Wait={wait}ms, \t END: {sw.ElapsedMilliseconds}ms");
        });
    }
}
Console.ReadKey();
return;
0 голосов
/ 17 декабря 2018

Я немного переписал опубликованный фрагмент, чтобы лучше упорядочить результаты, у моего нового ноутбука слишком много ядер, чтобы достаточно хорошо интерпретировать существующий перепутанный вывод.Запись времени начала и окончания каждой задачи и отображение их после того, как все они выполнены.И запись фактического времени начала Задачи.Я получил:

0: 68 - 5031
1: 69 - 5031
2: 68 - 5031
3: 69 - 5031
4: 69 - 1032
5: 68 - 5031
6: 68 - 5031
7: 69 - 5031
8: 1033 - 5031
9: 1033 - 2032
10: 2032 - 5031
11: 2032 - 3030
12: 3030 - 5031
13: 3030 - 4029
14: 4030 - 5031
15: 4030 - 5031

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

Это сценарий тупика, похожий на this Q + A , но безкатастрофический исход кода этого пользователя.Причину почти невозможно увидеть, поскольку она скрыта в коде .NETFramework, вам придется посмотреть, как реализован Task.Delay (), чтобы понять его.

Соответствующий код здесь , обратите внимание, как он использует System.Threading.Timer для реализации задержки.Важной деталью этого таймера является то, что его обратный вызов выполняется в пуле потоков.Это основной механизм, с помощью которого Task.Delay () может реализовать обещание «вы не платите за то, что вы не используете».

Важная деталь в том, что это может занять некоторое время, если пул потоковзанят обработкой запросов на выполнение потоков пула.Дело не в том, что таймер работает медленно, проблема в том, что метод обратного вызова не запускается достаточно быстро.Проблема в этой программе, Task.Run () добавила кучу запросов, больше чем может быть выполнено одновременно.Взаимная блокировка возникает из-за того, что tp-поток, запущенный Task.Run (), не может завершить вызов Wait () до тех пор, пока не будет выполнен обратный вызов таймера.

Вы можете сделать его жестким тупиком, который навсегда зависает, добавив программуэтот бит кода в начале Main ():

     ThreadPool.SetMaxThreads(Environment.ProcessorCount, 1000);

Но нормальные максимальные потоки намного выше.Какой менеджер потоков использует в своих интересах, чтобы решить эту тупиковую ситуацию.Раз в секунду он позволяет выполнять еще два потока, чем их «идеальное» число, когда существующие не завершаются.Это то, что вы видите на выходе.Но это только два за раз, этого недостаточно, чтобы положить большую часть вмятины в 8 занятых потоков, которые заблокированы при вызове Wait ().

В вызове Thread.Sleep () такой проблемы нет, это не зависит от кода .NETFramework или пула потоков для завершения.Это - планировщик потока OS, который заботится об этом, и он всегда работает в силу прерывания часов.Таким образом, позволяя новым потокам tp запускаться каждые 100 или 300 мсек, а не раз в секунду.

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

0 голосов
/ 08 декабря 2018

Ни Thread.Sleep(), ни Task.Delay() не гарантируют, что внутреннее будет правильным.

Thread.Sleep() работают Task.Delay() совсем по-другому.Thread.Sleep() блокирует текущий поток и не позволяет ему выполнять какой-либо код.Task.Delay() создает таймер, который будет тикать по истечении времени, и назначает его для выполнения в пуле потоков.

Вы запускаете свой код с помощью Task.Run(), который создает задачи и ставит их в очередь в пуле потоков.Когда вы используете Task.Delay(), текущий поток освобождается обратно в пул потоков, и он может начать обработку другой задачи.Таким образом, несколько задач начнутся быстрее, и вы запишете время запуска для всех.Затем, когда таймеры задержки начинают тикать, они также истощают пул, и выполнение некоторых задач занимает больше времени, чем с момента их запуска.Вот почему вы записываете длительное время.

Когда вы используете Thread.Sleep(), вы блокируете текущий поток в пуле, и он не может обрабатывать больше задач.Пул потоков не увеличивается сразу, поэтому новые задачи просто ждут.Поэтому все задачи выполняются примерно в одно и то же время, что кажется вам более быстрым.

РЕДАКТИРОВАТЬ: Вы используете Task.Wait().В вашем случае Task.Wait () пытается встроить выполнение в том же потоке.В то же время Task.Delay() использует таймер, который выполняется в пуле потоков.Один раз, вызвав Task.Wait(), вы блокируете рабочий поток из пула, во-вторых, вам требуется доступный поток в пуле для завершения операции того же рабочего метода.Когда вы await Delay(), такое встраивание не требуется, и рабочий поток сразу становится доступным для обработки событий таймера.Когда вы Thread.Sleep, у вас нет таймера для завершения рабочего метода.

Я полагаю, что именно это и вызывает резкую разницу в задержке.

...