在.NET 4.5中同时运行两个异步任务,并收集结果。

146
我已经尝试了一段时间,试图用.NET 4.5来完成一些我认为很简单的工作。
我想同时启动两个长时间运行的任务,并以最佳的C# 4.5(RTM)方式收集结果。
以下代码可以工作,但我不喜欢它,因为:
- 我希望Sleep成为一个异步方法,这样它可以等待其他方法。 - 使用Task.Run()看起来很笨拙。 - 我甚至不认为这使用了任何新的语言特性! 工作代码:
public static void Go()
{
    Console.WriteLine("Starting");

    var task1 = Task.Run(() => Sleep(5000));    
    var task2 = Task.Run(() => Sleep(3000));

    int totalSlept = task1.Result + task2.Result;

    Console.WriteLine("Slept for a total of " + totalSlept + " ms");
}

private static int Sleep(int ms)
{
    Console.WriteLine("Sleeping for " + ms);
    Thread.Sleep(ms);
    Console.WriteLine("Sleeping for " + ms + " FINISHED");
    return ms;
}

不工作的代码:

更新:实际上这段代码是有效的,也是正确的方法,唯一的问题是Thread.Sleep

这段代码不起作用,因为对Sleep(5000)的调用立即启动了任务运行,所以Sleep(1000)直到它完成后才运行。尽管Sleepasync的,我没有使用await或者过早调用.Result,这个问题仍然存在。

我以为也许有一种方法可以通过调用一个async方法来获得一个未运行的Task<T>,然后我可以在这两个任务上调用Start(),但是我无法弄清楚如何通过调用一个异步方法来获得一个Task<T>

public static void Go()
{
    Console.WriteLine("Starting");

    var task1 = Sleep(5000);    // blocks
    var task2 = Sleep(1000);

    int totalSlept = task1.Result + task2.Result;

    Console.WriteLine("Slept for " + totalSlept + " ms");
}

private static async Task<int> Sleep(int ms)
{
    Console.WriteLine("Sleeping for " + ms);
    Thread.Sleep(ms);
    return ms;
}

将Go语言变成异步方法并不会产生任何影响。 - Simon_Weaver
4
Block 是发生在 task1.Result 而不是 var task1 = Sleep(5000),因为你的 Sleep 方法没有使用 await 关键字,所以它是同步的。 - Arvis
这个问题很混乱。private static async Task<int> Sleep(int ms) 方法应该给你一个编译警告(CS1998),但你没有注意到。 - undefined
6个回答

154
async Task<int> LongTask1() { 
  ...
  return 0; 
}

async Task<int> LongTask2() { 
  ...
  return 1; 
}

...
{
   Task<int> t1 = LongTask1();
   Task<int> t2 = LongTask2();
   await Task.WhenAll(t1,t2);
   //now we have t1.Result and t2.Result
}

4
因为你将 t1 和 t2 声明为 Task 类型,这是正确的做法,所以我点了个赞(+1)。 - Won
12
我认为这个解决方案需要Go方法也是异步的,这意味着它具有异步的能力。如果您想要像提问者的情况一样,调用者的“Go”方法是同步的,但想要异步地完成两个独立的任务(即两个任务不必在另一个任务完成之前完成,但两个任务都必须在执行继续之前完成),那么使用**Task.WaitAll会更好,您不需要使用await关键字,因此也不需要调用“Go”方法本身是异步的。没有哪种方法更好,这只是取决于您的目标是什么。** - AaronLS
1
Void方法:“async void LongTask1() {...}”没有Task.Result属性。 在这种情况下使用不带T的Task:async Task LongTask1() - Arvis
我没有从任何一个任务中得到结果。所以我将其更改为 Task<TResult> t1 = LongTask1();,现在我可以得到 t1.Result<TResult> 是您结果的返回类型。您需要在方法中使用 return <TResult> 才能使其正常工作。 - gilu
@gilu,你显然是正确的,代码片段的主要目的是展示如何等待两个操作完成,但我在注释中提到了Result属性。我做了一点小改动,请现在看一下代码片段。 - Bart
1
值得一提的是,如果您正在执行一些非常简单的操作,并且不想要额外的“t1”和“t2”变量,可以使用“new Task(...)”。例如:int int1 = 0; await Task.WhenAll(new Task(async () => { int1 = await LongTask1(); }));。这种方法的一个问题是编译器将无法识别该变量是否已被分配,并且如果您没有给它一个初始值,它将被视为未分配。 - Robert Dennis

108
你应该在异步编程中使用Task.Delay而不是Sleep,然后使用Task.WhenAll来合并任务结果。这些任务将并行运行。
public class Program
{
    static void Main(string[] args)
    {
        Go();
    }

    public static void Go()
    {
        GoAsync();
        Console.ReadLine();
    }

    public static async void GoAsync()
    {
        Console.WriteLine("Starting");

        var task1 = Sleep(5000);
        var task2 = Sleep(3000);

        int[] result = await Task.WhenAll(task1, task2);

        Console.WriteLine("Slept for a total of " + result.Sum() + " ms");
    }

    private async static Task<int> Sleep(int ms)
    {
        Console.WriteLine("Sleeping for {0} at {1}", ms, Environment.TickCount);
        await Task.Delay(ms);
        Console.WriteLine("Sleeping for {0} finished at {1}", ms, Environment.TickCount);
        return ms;
    }
}

15
这是一个很好的回答...但在我运行它之前,我认为这个答案是错误的。然后我明白了。它确实可以在5秒内执行。诀窍在于不要立即等待任务完成,而是等待Task.WhenAll。 - Tim Lovell-Smith

4
尽管您的Sleep方法是异步的,但Thread.Sleep则不是。异步的整个理念在于重用单个线程,而不是启动多个线程。由于您使用同步调用的Thread.Sleep导致被阻塞,因此它将无法正常工作。
我假设Thread.Sleep是您实际想要执行的操作的简化表示。您的实际实现是否可以编写为异步方法?
如果您确实需要运行多个同步阻塞调用,请另寻他路!

谢谢Richard - 是的,当我实际使用我的服务调用时,它似乎按预期工作。 - Simon_Weaver
那么如何运行异步程序呢?我有一个应用程序,它会频繁切换文件并等待大约5秒钟,然后进行另一个进程。当我使用“when for all”时,它首先运行第一个进程,然后是第二个进程,即使我说:var x = y(),而不是var x=await y()y().wait(),但它仍然一直等待,如果异步不能自己处理,我该怎么办?请注意,y被装饰为异步,并且我希望它在所有操作中完成,而不是在分配的位置上完成。编辑:我刚刚和我的合作伙伴一起尝试了“Task.Factory”,他说当我跳出这个类时它可以工作。 - Hassan Faghihi

2
为了回答这个问题:
“我希望Sleep成为异步方法,这样它就可以等待其他方法”
你可以尝试像这样重写Sleep函数:
private static async Task<int> Sleep(int ms)
{
    Console.WriteLine("Sleeping for " + ms);
    var task = Task.Run(() => Thread.Sleep(ms));
    await task;
    Console.WriteLine("Sleeping for " + ms + "END");
    return ms;
}

static void Main(string[] args)
{
    Console.WriteLine("Starting");

    var task1 = Sleep(2000);
    var task2 = Sleep(1000);

    int totalSlept = task1.Result +task2.Result;

    Console.WriteLine("Slept for " + totalSlept + " ms");
    Console.ReadKey();
}

运行此代码将输出:
Starting
Sleeping for 2000
Sleeping for 1000
*(one second later)*
Sleeping for 1000END
*(one second later)*
Sleeping for 2000END
Slept for 3000 ms

1
现���是周末啦!
    public async void Go()
    {
        Console.WriteLine("Start fosterage...");

        var t1 = Sleep(5000, "Kevin");
        var t2 = Sleep(3000, "Jerry");
        var result = await Task.WhenAll(t1, t2);

        Console.WriteLine($"My precious spare time last for only {result.Max()}ms");
        Console.WriteLine("Press any key and take same beer...");
        Console.ReadKey();
    }

    private static async Task<int> Sleep(int ms, string name)
    {
            Console.WriteLine($"{name} going to sleep for {ms}ms :)");
            await Task.Delay(ms);
            Console.WriteLine("${name} waked up after {ms}ms :(";
            return ms;
    }

0

这篇文章帮助解释了很多问题,它是以FAQ的形式呈现。

Async/Await FAQ

这部分解释了为什么Thread.Sleep在同一原始线程上运行,导致了我的初始困惑。

“async”关键字是否会导致方法调用排队到线程池?创建新线程?发射火箭去火星?

不会。不会。也不会。请参见前面的问题。“async”关键字告诉编译器,在方法内可以使用“await”,因此当等待实例完成时,该方法可能会在等待点挂起并异步恢复执行。这就是为令编译器在标记为“async”的方法中没有“awaits”时发出警告的原因。


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