Cara menggunakan await dalam satu loop

87

Saya mencoba membuat aplikasi konsol asinkron yang melakukan beberapa pekerjaan pada koleksi. Saya memiliki satu versi yang menggunakan paralel for loop versi lain yang menggunakan async / await. Saya mengharapkan versi async / await berfungsi mirip dengan versi paralel tetapi dijalankan secara sinkron. Apa yang saya lakukan salah?

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;
    }
}
Satish
sumber

Jawaban:

126

Cara Anda menggunakan awaitkata kunci memberi tahu C # bahwa Anda ingin menunggu setiap kali Anda melewati loop, yang tidak sejajar. Anda dapat menulis ulang metode Anda seperti ini untuk melakukan apa yang Anda inginkan, dengan menyimpan daftar Taskdan kemudian awaitmemasukkan semuanya 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);
}
Tim S.
sumber
3
Saya tidak tahu tentang orang lain, tetapi paralel untuk / foreach tampaknya lebih lurus ke depan untuk loop paralel.
Brettski
8
Penting untuk diperhatikan bahwa saat Anda melihat Ending Processnotifikasi bukan saat tugas benar-benar berakhir. Semua pemberitahuan tersebut dibuang secara berurutan tepat setelah tugas terakhir selesai. Pada saat Anda melihat "Mengakhiri Proses 1", proses 1 mungkin sudah lama berakhir. Selain pilihan kata yang ada, +1.
Asad Saeeduddin
@Brettski Saya mungkin salah, tetapi loop paralel menjebak segala jenis hasil asinkron. Dengan mengembalikan Tugas <T>, Anda segera mendapatkan kembali objek Tugas di mana Anda dapat mengelola pekerjaan yang sedang berlangsung di dalamnya seperti membatalkannya atau melihat pengecualian. Sekarang dengan Async / Await Anda dapat bekerja dengan objek Task dengan cara yang lebih bersahabat - yaitu Anda tidak perlu melakukan Task.Result.
The Muffin Man
@ Tim S, bagaimana jika saya ingin mengembalikan nilai dengan fungsi async menggunakan metode Tasks.WhenAll?
Mihir
Apakah praktik yang buruk untuk menerapkan Semaphorein DoWorkAsyncuntuk membatasi tugas eksekusi maksimum?
C4d
39

Kode Anda menunggu untuk setiap operasi (menggunakan await) selesai sebelum memulai iterasi berikutnya.
Oleh karena itu, Anda tidak mendapatkan paralelisme apa pun.

Jika Anda ingin menjalankan operasi asinkron yang ada secara paralel, Anda tidak perlu await; Anda hanya perlu mendapatkan sekumpulan Tasks dan panggilan Task.WhenAll()untuk mengembalikan tugas yang menunggu semuanya:

return Task.WhenAll(list.Select(DoWorkAsync));
SLaks
sumber
jadi Anda tidak dapat menggunakan metode asinkron apa pun dalam loop apa pun?
Sabish
4
@ Satish: Anda bisa. Namun, awaittidak sebaliknya dari apa yang Anda inginkan - itu menunggu untuk Taskselesai.
SLaks
Saya ingin menerima jawaban Anda, tetapi Tims S memiliki jawaban yang lebih baik.
Sabish
Atau jika Anda tidak perlu tahu kapan tugas selesai Anda dapat memanggil metode tanpa menunggu mereka
disklosr
Untuk mengonfirmasi apa yang dilakukan sintaks itu - menjalankan Tugas yang dipanggil DoWorkAsyncpada setiap item masuk list(meneruskan setiap item DoWorkAsync, yang saya asumsikan memiliki parameter tunggal)?
jbyrd
12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}
Vladimir
sumber
4

Di C # 7.0 Anda dapat menggunakan nama semantik untuk setiap anggota tupel , berikut adalah jawaban Tim S. menggunakan sintaks baru:

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);
}

Anda juga bisa menyingkirkan bagian task. dalamforeach :

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