ループでawaitを使用する方法


86

コレクションに対していくつかの作業を行う非同期コンソールアプリを作成しようとしています。並列forループを使用するバージョンとasync / awaitを使用するバージョンがあります。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;
    }
}

回答:


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を実行する必要はありません。
マフィンマン

@Tim S、Tasks.WhenAllメソッドを使用して非同期関数で値を返したい場合はどうなりますか?
mihir 2018

実行タスクの最大数を制限するためにSemaphoreinDoWorkAsyncを実装するのは悪い習慣でしょうか?
c4d 2018年

39

コードは、(を使用してawait)各操作が終了するのを待ってから、次の反復を開始します。
したがって、並列処理は行われません。

既存の非同期操作を並行して実行する場合はawait、;は必要ありません。のコレクションを取得し、それらすべてを待機するタスクを返すためにTask呼び出す必要がTask.WhenAll()あります。

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

したがって、ループで非同期メソッドを使用することはできませんか?
土曜日2013年

4
@Satish:できます。しかし、awaitあなたが望むこととは正反対のことをします–それは終了するのを待ちますTask
SLaks 2013年

私はあなたの答えを受け入れたかったのですが、ティムスSの方が良い答えです。
土曜日2013年

または、タスクがいつ終了したかを知る必要がない場合は、メソッドを待たずにメソッドを呼び出すことができます
disklosr 2016年

その構文が何をしているのかを確認するために-のDoWorkAsync各アイテムで呼び出されたタスクを実行しています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では、あなたがタプルの各メンバーにセマンティック名を使用することができ、ここにあるティム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);
    }
}
// ...
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.