如何在循环中使用等待


86

我正在尝试创建一个异步控制台应用程序,该应用程序在集合上做一些工作。我有一个使用并行循环的版本,另一个使用异步/等待的版本。我期望async / await版本的工作方式类似于并行版本,但是它是同步执行的。我究竟做错了什么?

class Program
{
    static void Main(string[] args)
    {
        var worker = new Worker();
        worker.ParallelInit();
        var t = worker.Init();
        t.Wait();
        Console.ReadKey();
    }
}

public class Worker
{
    public async Task<bool> Init()
    {
        var series = Enumerable.Range(1, 5).ToList();
        foreach (var i in series)
        {
            Console.WriteLine("Starting Process {0}", i);
            var result = await DoWorkAsync(i);
            if (result)
            {
                Console.WriteLine("Ending Process {0}", i);
            }
        }

        return true;
    }

    public async Task<bool> DoWorkAsync(int i)
    {
        Console.WriteLine("working..{0}", i);
        await Task.Delay(1000);
        return true;
    }

    public bool ParallelInit()
    {
        var series = Enumerable.Range(1, 5).ToList();
        Parallel.ForEach(series, i =>
        {
            Console.WriteLine("Starting Process {0}", i);
            DoWorkAsync(i);
            Console.WriteLine("Ending Process {0}", i);
        });
        return true;
    }
}

Answers:


123

您使用await关键字的方式告诉C#您希望在每次循环时都等待,这不是并行的。您可以通过存储Tasks的列表,然后使用await来重写它们,从而像这样重写您的方法以执行所需的操作Task.WhenAll

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<Tuple<int, bool>>>();
    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }
    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.Item2)
        {
            Console.WriteLine("Ending Process {0}", task.Item1);
        }
    }
    return true;
}

public async Task<Tuple<int, bool>> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return Tuple.Create(i, true);
}

3
我对其他人一无所知,但是对于并行循环,并行for / foreach似乎更直接。
Brettski 2014年

8
重要的是要注意,当您看到Ending Process通知不是在任务实际结束时才通知您。所有这些通知都将在最后一个任务完成后按顺序转储。当您看到“结束进程1”时,进程1可能已经结束了很长时间。除了那里的单词选择外,+ 1。
Asad Saeeduddin 2014年

@Brettski我可能是错的,但是并行循环会捕获任何类型的异步结果。通过返回Task <T>,您将立即返回Task对象,您可以在其中管理正在进行的工作,例如取消它或查看异常。现在,通过Async / Await,您可以以更友好的方式使用Task对象-即您不必执行Task.Result。
松饼人2015年

@Tim S,如果我想使用Tasks.WhenAll方法使用异步函数返回值怎么办?
米希尔(Mihir)'18 -4-10

实施SemaphoreinDoWorkAsync来限制最大执行任务会是不好的做法吗?
C4d '18年

39

您的代码等待每个操作(使用await)完成,然后再开始下一次迭代。
因此,您不会获得任何并行性。

如果要并行运行现有的异步操作,则不需要await;您只需要获取的集合Task并调用Task.WhenAll()以返回等待所有任务的任务:

return Task.WhenAll(list.Select(DoWorkAsync));

所以您不能在任何循环中使用任何异步方法吗?
2013年

4
@Satish:可以。然而,await确实你想要什么的完全相反-它等待Task结束。
SLaks 2013年

我想接受您的回答,但Tims S的回答更好。
2013年

或者,如果您不需要知道任务何时完成,则可以在不等待方法的情况下调用这些方法
disklosr

为了确认该语法在做什么-它正在运行DoWorkAsync在中的每个项目上调用的Task list(将每个项目传递到中DoWorkAsync,我假设它具有一个参数)?
jbyrd

12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}

4

在C#7.0中,您可以对元组的每个成员使用语义名称,这是Tim S.使用新语法的答案:

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<(int Index, bool IsDone)>>();

    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }

    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.IsDone)
        {
            Console.WriteLine("Ending Process {0}", task.Index);
        }
    }

    return true;
}

public async Task<(int Index, bool IsDone)> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return (i, true);
}

您还可以摆脱task. 里面foreach

// ...
foreach (var (IsDone, Index) in await Task.WhenAll(tasks))
{
    if (IsDone)
    {
        Console.WriteLine("Ending Process {0}", Index);
    }
}
// ...
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.