Метод Wait() не нужен для задачи, возвращаемой методом ContinueWith?

#c# #task

Вопрос:

Почему код работает, даже если я не использую Wait() метод для второй задачи, возвращаемой методом ContinueWith ?

Если я не использую Wait() метод для первой задачи, то ничего не сработает, но не для второй.

 using System;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            void MyMethod1()
            {
                Console.WriteLine(1);
            }

            void MyMethod2(Task MyTask)
            {
                Console.WriteLine(2);
            }

            Task MyTask1 = new Task(MyMethod1);

            MyTask1.Start();
            MyTask1.Wait();

            Task MyTask2 = MyTask1.ContinueWith(MyMethod2); //Why it is work?
            //MyTask2.Wait();
        }
    }
}
 

Комментарии:

1. Вы пробовали искать в Google ContinueWith ? Первым хитом для меня стала документация, которая четко объясняет, что происходит. docs.microsoft.com/en-us/dotnet/api/…

2. Вы вообще не должны использовать .Wait() . Итак: перестаньте это делать. Кроме того, настоятельно не рекомендуется использовать конструктор задач, который принимает делегат. В зависимости от того, что вы пытаетесь здесь сделать: вероятно Task.Run , и await ваши друзья, возможно, с точкой static async Task Main() входа. Вам тоже не должно быть нужно .ContinueWith — просто await

3. @Enigmativity, Если я задал вопрос, это означает, что я не понимаю, можете ли вы написать начало текста из статьи, которая все объясняет?

4. Дополнительная мысль: ничто в коде в вопросе не приносит отдаленной пользы от участия в задаче. Я собирался написать пример с использованием await , но : здесь действительно нечего ждать , если только вы намеренно не переключаете работу между потоками (например, чтобы разблокировать поток пользовательского интерфейса, который не применяется в случае консольного exe). Я не пытаюсь быть пренебрежительным здесь — совсем наоборот. Если вы можете пояснить, почему вы используете здесь задачи, возможно, будет более разумный и наглядный пример.

5. Спасибо за ответы, но я еще не добрался до главы об асинхронном ожидании.

Ответ №1:

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

Если вы добавите Console.ReadLine() в конец метода и попытаетесь запустить его без каких .Wait() -либо изменений, вы увидите, что он выводится 1 и 2 , как и ожидалось.

Итак, почему же это происходит?

Ну, это потому, что с первой записью на консоль связаны накладные расходы — накладные расходы, которые не возникают при последующих записях.

Поэтому, когда у вас нет Wait() первого Console.WriteLine() , вы не успели закончить до выхода программы.

Однако, когда вы добавляете Wait() к первой задаче, первая Console.WriteLine() (очевидно) завершена, а вторая Console.WriteLine() не имеет накладных расходов, связанных с первой, и у нее есть время закончить до выхода из программы.

(Это условие гонки, и фактические результаты могут отличаться в разных пробегах и в разных средах.)

Мы можем выполнить некоторые элементарные тайминги, чтобы показать разницу во времени, затраченном между первым вызовом Console.WriteLine() и вторым вызовом. (Примечание: Обычно я использую скамейку.Нетто для таймингов, но это не очень хорошо сработало бы в данном конкретном случае.)

 static void Main()
{
    Stopwatch sw = Stopwatch.StartNew();
    Console.WriteLine("1");
    sw.Stop();
    Console.WriteLine("First Console.WriteLine() took "   sw.Elapsed);

    sw.Restart();
    Console.WriteLine("2");
    sw.Stop();
    Console.WriteLine("Second Console.WriteLine() took "   sw.Elapsed);
}
 

Для сборки релиза на моем ПК это выводит:

 1
First Console.WriteLine() took 00:00:00.0060951
2
Second Console.WriteLine() took 00:00:00.0000624
 

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

Но если a Console.WriteLine() ранее был выполнен, программа должна будет выйти менее чем 0.0000624 за секунды, чтобы вывод не появился (приблизительно!).

Вы также можете продемонстрировать это, добавив a Console.WriteLine() в начало программы, чтобы последующие вызовы избегали накладных расходов при первом вызове:

 Console.WriteLine("Priming");

void MyMethod1()
{
    Console.WriteLine(1);
}

void MyMethod2(Task MyTask)
{
    Console.WriteLine(2);
}

Task MyTask1 = new Task(MyMethod1);
MyTask1.Start();
Task MyTask2 = MyTask1.ContinueWith(MyMethod2); //Why it is work?
 

Если вы это сделаете, вывод будет 1 и 2 — даже без вызова MyTask1.Wait(); .

Опять же, обратите внимание, что это условие гонки, и фактические результаты могут отличаться. Он полагается на вызовы для WriteLine() завершения до фактического завершения программы.

Ваш вывод из этого должен заключаться в том, что многопоточность сложна, и вы должны делать это правильно, иначе будут происходить странные вещи, подобные этому!

Комментарии:

1. Это стало немного понятнее для меня, но остались проблемы с точки зрения ресурсов процессора. Это мои догадки, но, возможно, вы сможете уточнить. Вариант 1. Если мы создадим одну задачу и запланируем ее с помощью метода «Пуск» и ничего больше — в консольном приложении, основной метод завершит работу до запуска нашей задачи. Потому что задача должна выполняться в новом потоке, а не в вызывающем потоке, и выделение ресурсов для нового потока занимает больше времени, чем требуется для завершения основного метода в вызывающем потоке. Правильно ли я понимаю?

2. Вариант 2. Мы используем вариант 1, но вызываем метод Wait из нашей задачи, который заставляет вызывающий поток ждать, пока наша задача не завершится в новом потоке. Здесь я уверен, что я прав. Вариант 3. Мы используем вариант 1, но сначала запускаем консоль. WriteLine («Грунтовка»), как вы написали. Тогда задача завершится в том же потоке, что и работа основного метода, то есть вызывающего, потому что для него уже выделены ресурсы? Это вопрос, правильно ли я понимаю?

3. Вариант 4. Мы используем вариант 2, из задачи мы называем MyTask1. Продолжайте с (MyMethod2), и мы также видим, что все работает даже без MyTask1. ContinueWith (MyMethod2).Wait(), также в отдельном потоке, в котором работала задача MyTask1, а не вызывающий поток метода main только потому, что ресурсы уже выделены для работы MyTask1. Правильно ли я понимаю? Бывают ли такие вещи?

4. Другими словами, чтобы гарантировать выполнение задачи, возвращающей ContinueWith (), нужно ли использовать метод ожидания?

5. @NikVladi Это правильно.