Как объявить незапущенную задачу, которая будет ожидать другую задачу?

9

Я провел этот модульный тест, и я не понимаю, почему "await Task.Delay ()" не ждет!

   [TestMethod]
    public async Task SimpleTest()
    {
        bool isOK = false;
        Task myTask = new Task(async () =>
        {
            Console.WriteLine("Task.BeforeDelay");
            await Task.Delay(1000);
            Console.WriteLine("Task.AfterDelay");
            isOK = true;
            Console.WriteLine("Task.Ended");
        });
        Console.WriteLine("Main.BeforeStart");
        myTask.Start();
        Console.WriteLine("Main.AfterStart");
        await myTask;
        Console.WriteLine("Main.AfterAwait");
        Assert.IsTrue(isOK, "OK");
    }

Вот результат модульного теста:

Тест модульного вывода

Как это возможно, «ожидание» не ждет, и основной поток продолжается?

Elo
источник
Немного неясно, чего вы пытаетесь достичь. Можете ли вы добавить ожидаемый результат?
OlegI
1
Метод тестирования очень понятен - ожидается, что isOK будет правдой
сэр Руфо
Нет причин создавать незапущенную задачу. Задачи не являются потоками, они используют потоки. Что ты пытаешься сделать? Почему бы не использовать Task.Run()после первого Console.WriteLine?
Панайотис Канавос
1
@ Эло, ты только что описал пулы потоков. Вам не нужен пул задач для реализации очереди заданий. Вам нужна очередь заданий, например, объекты Action <T>
Panagiotis Kanavos
2
@Эло, что вы пытаетесь сделать, уже доступно в .NET, например, через классы потока данных TPL, такие как ActionBlock, или более новые классы System.Threading.Channels. Вы можете создать ActionBlock для получения и обработки сообщений, используя одну или несколько одновременных задач. Все блоки имеют входные буферы с настраиваемой емкостью. DOP и пропускная способность позволяют вам контролировать параллелизм, регулировать запросы и создавать противодавление - если слишком много сообщений находятся в очереди, производитель ожидает
Panagiotis Kanavos

Ответы:

8

new Task(async () =>

Задача не занимает Func<Task>, а Action. Он будет вызывать ваш асинхронный метод и ожидать, что он завершится, когда вернется. Но это не так. Это возвращает задачу. Эта задача не ожидается новой задачей. Для новой задачи задание выполняется после возврата метода.

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

[TestMethod]
public async Task SimpleTest()
{
    bool isOK = false;

    Func<Task> asyncMethod = async () =>
    {
        Console.WriteLine("Task.BeforeDelay");
        await Task.Delay(1000);
        Console.WriteLine("Task.AfterDelay");
        isOK = true;
        Console.WriteLine("Task.Ended");
    };

    Console.WriteLine("Main.BeforeStart");
    Task myTask = asyncMethod();

    Console.WriteLine("Main.AfterStart");

    await myTask;
    Console.WriteLine("Main.AfterAwait");
    Assert.IsTrue(isOK, "OK");
}
nvoigt
источник
4
Task.Run(async() => ... )также вариант
сэр Руфо
Вы просто сделали то же самое, что и автор вопроса
OlegI
Кстати myTask.Start();, поднятьInvalidOperationException
сэр Руфо
@ Олег Я этого не вижу. Не могли бы вы объяснить это, пожалуйста?
Nvoigt
@nvoigt Я предполагаю, что он подразумевает, myTask.Start()что даст исключение для его альтернативы и должен быть удален при использовании Task.Run(...). Там нет ошибки в вашем решении.
404
3

Проблема в том, что вы используете неуниверсальный Taskкласс, который не предназначен для получения результата. Поэтому, когда вы создаете Taskэкземпляр, передающий асинхронный делегат:

Task myTask = new Task(async () =>

... делегат рассматривается как async void. An async voidне является Task, его нельзя ожидать, его исключение не может быть обработано, и это источник тысяч вопросов, задаваемых разочарованными программистами здесь, в StackOverflow и в других местах. Решение состоит в том, чтобы использовать универсальный Task<TResult>класс, потому что вы хотите вернуть результат, а результат - другой Task. Итак, вы должны создать Task<Task>:

Task<Task> myTask = new Task<Task>(async () =>

Теперь, когда вы Startвнешнее, Task<Task>оно будет завершено почти мгновенно, потому что его задача - просто создать внутреннее Task. Затем вам придется ждать и внутреннего Task. Вот как это можно сделать:

myTask.Start();
Task myInnerTask = await myTask;
await myInnerTask;

У вас есть две альтернативы. Если вам не нужна явная ссылка на внутреннее, Taskтогда вы можете просто ждать внешнего Task<Task>дважды:

await await myTask;

... или вы можете использовать встроенный метод расширения, Unwrapкоторый объединяет внешние и внутренние задачи в одну:

await myTask.Unwrap();

Это развертывание происходит автоматически, когда вы используете гораздо более популярный Task.Runметод, который создает горячие задачи, поэтому в Unwrapнастоящее время он используется не очень часто.

Если вы решите, что ваш асинхронный делегат должен вернуть результат, например, a string, то вы должны объявить myTaskпеременную типа Task<Task<string>>.

Примечание: я не поддерживаю использование Taskконструкторов для создания холодных задач. Как правило, практика не одобряется по причинам, которые я на самом деле не знаю, но, вероятно, потому, что она используется настолько редко, что потенциально может застать врасплох других неосведомленных пользователей / сопровождающих / рецензентов кода.

Общий совет: будьте осторожны каждый раз, когда вы предоставляете асинхронный делегат в качестве аргумента метода. Этот метод в идеале должен ожидать Func<Task>аргумент (что означает, что он понимает асинхронные делегаты) или, по крайней мере, Func<T>аргумент (то есть, что, по крайней мере, сгенерированный Taskне будет проигнорирован). В неудачном случае, когда этот метод принимает Action, ваш делегат будет рассматриваться как async void. Это редко то, что вы хотите, если когда-либо.

Теодор Зулиас
источник
Как подробный технический ответ! благодарю вас.
Эло
@Эло мое удовольствие!
Теодор Зулиас
1
 [Fact]
        public async Task SimpleTest()
        {
            bool isOK = false;
            Task myTask = new Task(() =>
            {
                Console.WriteLine("Task.BeforeDelay");
                Task.Delay(3000).Wait();
                Console.WriteLine("Task.AfterDelay");
                isOK = true;
                Console.WriteLine("Task.Ended");
            });
            Console.WriteLine("Main.BeforeStart");
            myTask.Start();
            Console.WriteLine("Main.AfterStart");
            await myTask;
            Console.WriteLine("Main.AfterAwait");
            Assert.True(isOK, "OK");
        }

введите описание изображения здесь

BASKA
источник
3
Вы понимаете, что без await, задержка задачи фактически не задержит эту задачу, верно? Вы удалили функциональность.
Nvoigt
Я только что проверил, @nvoigt прав: истекшее время: 0: 00: 00,0106554. И мы видим на вашем скриншоте: «Истекшее время: 18 мс», оно должно быть> = 1000 мс
Elo
Да, вы правы, я обновляю свой ответ. Tnx. После ваших комментариев я решаю это с минимальными изменениями. :)
BASKA
1
Хорошая идея использовать wait () и не ждать изнутри Задачи! Спасибо !
Эло