如何启动异步任务对象

3

我希望同时启动一个Task对象的集合,并等待它们全部完成。以下代码展示了我的期望行为。

public class Program
{
    class TaskTest
    {
        private Task createPauseTask(int ms)
        {
            // works well
            return Task.Run(async () =>
            // subsitution: return new Task(async () =>
            {
                Console.WriteLine($"Start {ms} ms pause");
                await Task.Delay(ms);
                Console.WriteLine($"{ms} ms are elapsed");
            });
        }

        public async Task Start()
        {
            var taskList= new List<Task>(new[]
            {
                createPauseTask(1000),
                createPauseTask(2000)
            });
            // taskList.ForEach(x => x.Start());
            await Task.WhenAll(taskList);
            Console.WriteLine("------------");
        }
    }

    public static void Main()
    {
        var t = new TaskTest();
        Task.Run(() => t.Start());
        Console.ReadKey();
    }
}

输出结果为:
Start 1000 ms pause
Start 2000 ms pause
1000 ms are elapsed
2000 ms are elapsed
------------

现在我想知道是否可能单独准备我的任务并开始它们。为了实现这一点,我将createPauseTask(int ms)方法中的第一行从return Task.Run(async () =>更改为return new Task(async () =>Start()方法中,我在WhenAll之前包含了taskList.ForEach(x => x.Start());。但是,可怕的事情发生了。调用WhenAll会立即完成,并且输出如下:
Start 2000 ms pause
Start 1000 ms pause    
------------    
1000 ms are elapsed
2000 ms are elapsed

有人能帮我看看我的错误并告诉我如何修复吗?


乍一看,它看起来应该能正常工作,所以我尝试了你的完整代码,在我的机器上也能正常运行。两个版本的行为基本相同。可能是环境问题吗?比如.NET版本等。我使用的是4.7版本。 - sellotape
@selltape 对我来说,按照OP的描述工作正常:问题出在打印虚线(代表Task.WhenAll返回)的位置。 - pere57
@pere57 - 啊,是的,很微妙。我理解 "The WhenAll call completes immediately and the output is..." 的意思是 "1000 ms are elapsed" 和 "2000 ms are elapsed" 会立即打印出来,但实际上它们分别在1秒和2秒后才打印出来。在我的情况下,Task.WhenAll() 确实在任务完成之前返回。 - sellotape
未开始的任务是一个不好的模式。无论你想要实现什么,通常都可以用不同的方式更好地实现。 - usr
2个回答

8
问题在于您正在使用的Task构造函数接受一个Action委托。当您这样说时:
var task = new Task(async () => { /* Do things */ });

您创建的不是一个“做事情”的任务,而是创建了一个执行返回任务的动作的任务,并且这个(内部)任务就是“做事情”的。创建这个(内部)任务非常快,因为委托在第一个await时返回Task并几乎立即完成。由于委托是一个Action,所以产生的Task实际上被丢弃了,现在不能再用来等待。
当您在外部任务上调用await Task.WhenAll(tasks)时,您只等待内部任务被创建(几乎立即)。内部任务之后继续运行。
有构造函数重载允许您做您想要的事情,但您的语法会有点笨拙,类似于Paulo的答案:
public static Task<Task> CreatePauseTask(int ms)
{
    return new Task<Task>(async () =>
        {
            Console.WriteLine($"Start {ms} ms pause");
            await Task.Delay(ms);
            Console.WriteLine($"{ms} ms are elapsed");
        });
}

现在你有一个任务,它与之前的任务相同,但这次返回内部的Task。为了等待内部任务的完成,你可以这样做:

await Task.WhenAll(await Task.WhenAll(taskList));

内部的 await 返回内部任务列表,而外部的 await 等待它们。


正如之前提到的,使用构造函数创建未启动的任务只有在您具有高度特定的要求时才应该这样做,其中更常见的做法是使用 Task.Run() 或仅调用返回任务的方法来满足要求(这在99.99%的情况下都可以满足要求)。

大多数这些 Task 构造函数是在异步等待甚至不存在之前创建的,可能仅出于遗留原因而存在。


编辑:另一个有用的提示是查看两个委托的签名,C# 的 lambda 符号允许我们通常轻松地忽略这一点。

对于 new Task(async () => { /* Do things */ }) 我们有:

async void Action() { }

主要问题在于 void

对于 new Task<Task>(async () => { /* 做一些事情 */ }),我们有:

async Task Function() { }

这两个lambda表达式在语法上完全相同,但在语义上有所不同。


1
你也可以这样写:await Task.WhenAll(taskList.Select(outer => outer.Unwrap())); - pere57

2

我不知道为什么你如此喜欢使用 Task.Run。在你的代码中每次使用它都是没有必要的。

我也好奇你从哪里看到使用 Task 构造函数是一种推荐做法。

无论如何,如果不使用 Task 构造函数,代码会像这样:

class TaskTest
{
    private async Task CreatePauseTask(int ms)
    {
        Console.WriteLine($"Start {ms} ms pause");
        await Task.Delay(ms);
        Console.WriteLine($"{ms} ms are elapsed");
    }

    public async Task Start()
    {
        var taskList = new List<Task>(new[]
        {
                CreatePauseTask(1000),
                CreatePauseTask(2000)
            });
        await Task.WhenAll(taskList);
        Console.WriteLine("------------");
    }
}


static void Main()
{
    var t = new TaskTest();
    t.Start();
    Console.ReadKey();
}

这将产生以下输出:

Start 1000 ms pause
Start 2000 ms pause
1000 ms are elapsed
2000 ms are elapsed
------------

使用 Task 构造函数的方式如下:

class TaskTest
{
    private Task CreatePauseTask(int ms)
    {
        return new Task<Task>(async () =>
        {
            Console.WriteLine($"Start {ms} ms pause");
            await Task.Delay(ms);
            Console.WriteLine($"{ms} ms are elapsed");
        }, TaskCreationOptions.DenyChildAttach);
    }

    public async Task Start()
    {
        var taskList = new List<Task>(new[]
        {
                CreatePauseTask(1000),
                CreatePauseTask(2000)
            });
        taskList.ForEach(t => t.Start(TaskScheduler.Default));
        await Task.WhenAll(taskList);
        Console.WriteLine("------------");
    }
}


static void Main()
{
    var t = new TaskTest();
    t.Start();
    Console.ReadKey();
}

然后输出:

Start 1000 ms pause
Start 2000 ms pause
------------
1000 ms are elapsed
2000 ms are elapsed

这里的问题在于Task.Run了解返回Task的函数,而Task构造函数则不了解。因此,使用Task构造函数将调用该函数,并且在第一个阻止等待(await Task.Delay(ms);)后返回。
这是预期的行为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接