【问题标题】:How to start async Task objects如何启动异步任务对象
【发布时间】:2018-10-12 15:28:43
【问题描述】:

我想同时开始收集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)method 中的第一行从return Task.Run(async () =&gt; 更改为return new Task(async () =&gt;

Start() 方法中,我在WhenAll 之前添加了taskList.ForEach(x =&gt; x.Start());。但随后可怕的事情发生了。 WhenAll 调用立即完成,输出为:

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

谁能告诉我我的错误是什么以及如何解决它?

【问题讨论】:

  • 乍一看它应该可以正常工作,所以我尝试了您的确切代码,它在我的机器上工作正常。两个版本的行为基本相同。也许是环保的东西? .NET 版本等。我用的是 4.7
  • @sellotape 按照 OP 对我的描述工作:问题是虚线(代表 Task.WhenAll 的返回)被打印的位置。
  • @pere57 - 是的,很微妙。我读到“WhenAll 调用立即完成并且输出是......”意味着“1000 毫秒已过”和“2000 毫秒已过”立即打印,而它们实际上分别在 1 秒和 2 秒后打印。在我的Task.WhenAll() 确实在任务完成之前返回。
  • 未开始的任务是一种糟糕的模式。无论您想完成什么,通常都可以通过不同的方式更好地完成。

标签: c# async-await task


【解决方案1】:

问题是您使用的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));

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


正如也提到的,虽然 - 使用构造函数创建未启动的任务是一个领域,只有当你有一个高度特定的要求时,你才应该在其中使用Task.Run() 或简单地调用任务返回方法的更标准做法确实如此不符合要求(99.99% 的情况下都会这样做)。

大多数 Task 构造函数是在 async-await 存在之前创建的,并且可能只是由于遗留原因而仍然存在。


编辑:查看 2 个委托的签名可能也有帮助,C# lambda 表示法允许我们(通常很方便地)忽略这些签名。

对于new Task(async () =&gt; { /* Do things */ }),我们有

async void Action() { }

void 是这里的主要问题)。

对于new Task&lt;Task&gt;(async () =&gt; { /* Do things */ }),我们有

async Task Function() { }

两个 lambda 在语法上相同,但在语义上不同。

【讨论】:

  • 你也可以写await Task.WhenAll(taskList.Select(outer =&gt; outer.Unwrap()));
【解决方案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);)。

这是预期的行为。

【讨论】:

    猜你喜欢
    • 2019-12-20
    • 2012-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多