Як оголосити не розпочату задачу, яка буде чекати іншого завдання?


9

Я зробив цю тестову одиницю, і я не розумію, чому "очікування 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");
    }

Ось блок Тестовий вихід:

Вихідне випробування блоку

Як це можливо, "очікування" не чекає, а основний потік продовжується?


Трохи незрозуміло, чого ви намагаєтесь досягти. Чи можете ви, будь ласка, додати очікуваний результат?
ОлегІ

1
Метод тестування дуже чіткий - очікується, що правда будеOK є правдою
сер Руфо

Немає підстав створювати незапущене завдання. Завдання - це не нитки, вони використовують нитки. Що ти намагаєшся зробити? Чому б не використовувати Task.Run()після першого Console.WriteLine?
Панайотис Канавос

1
@Elo, ви щойно описали потокові пули. Для впровадження черги на роботу вам не потрібен пул завдань. Вам потрібна черга робочих місць, наприклад, об’єкти Action <T>
Panagiotis Kanavos

2
@Elo те, що ви намагаєтеся зробити, вже доступне в .NET, наприклад, через TPL-клас потоків даних, як ActionBlock, або новіші класи System.Threading.Channels. Ви можете створити ActionBlock для отримання та обробки повідомлень за допомогою однієї або декількох одночасних завдань. Усі блоки мають вхідні буфери з можливістю налаштування ємності. DOP та потужність дозволяють контролювати паралельність, запити на дросель та здійснювати зворотний тиск - якщо занадто багато повідомлень у черзі, виробник чекає
Panagiotis Kanavos

Відповіді:


8

new Task(async () =>

Завдання приймає не a Func<Task>, а an 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");
}

4
Task.Run(async() => ... )також варіант
сер Руфо

Ви просто зробили те саме, що і автор запитання
ОлегІ

BTW myTask.Start();піднімеInvalidOperationException
сер Руфо

@OlegI я цього не бачу. Чи можете ви поясніть це, будь ласка?
nvoigt

@nvoigt Я припускаю, що він означає, що він myTask.Start()створив би виняток для його альтернативи, і його слід видалити при використанні Task.Run(...). У вашому рішенні помилок немає.
404

3

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

Task myTask = new Task(async () =>

... делегат трактується як async void. Це 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. Це рідко, чого ви хочете, якщо і коли-небудь.


Як детальна технічна відповідь! Дякую тобі.
Ело

@Elo моє задоволення!
Теодор Зуліяс

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");
        }

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


3
Ви розумієте, що без awaitзатримки завдання фактично не затримає це завдання, правда? Ви видалили функціональність.
nvoigt

Я щойно перевірив, @nvoigt має рацію: Закінчений час: 0: 00: 00,0106554. І на вашому знімку ми бачимо: "минув час: 18 мс", це повинно бути> = 1000 мс
Ело

Так, ви праві, я оновлюю свою відповідь. Tnx. Після ваших коментарів я вирішую це з мінімальними змінами. :)
БАСКА

1
Гарна ідея використовувати wait (), а не чекати зсередини Завдання! Дякую !
Ело
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.