Bagaimana cara menggunakan Async dengan ForEach?

123

Apakah mungkin menggunakan Async saat menggunakan ForEach? Di bawah ini adalah kode yang saya coba:

using (DataContext db = new DataLayer.DataContext())
{
    db.Groups.ToList().ForEach(i => async {
        await GetAdminsFromGroup(i.Gid);
    });
}

Saya mendapatkan kesalahan:

Nama 'Async' tidak ada dalam konteks saat ini

Metode yang menyertakan pernyataan using disetel ke async.

James Jeffery
sumber

Jawaban:

180

List<T>.ForEachtidak bermain dengan baik dengan async(begitu pula LINQ-to-object, karena alasan yang sama).

Dalam kasus ini, saya sarankan untuk memproyeksikan setiap elemen ke dalam operasi asinkron, dan Anda kemudian dapat (secara asinkron) menunggu semuanya selesai.

using (DataContext db = new DataLayer.DataContext())
{
    var tasks = db.Groups.ToList().Select(i => GetAdminsFromGroupAsync(i.Gid));
    var results = await Task.WhenAll(tasks);
}

Manfaat dari pendekatan ini dibandingkan memberikan asyncdelegasi ForEachadalah:

  1. Penanganan error lebih tepat. Pengecualian dari async voidtidak bisa ditangkap dengan catch; pendekatan ini akan menyebarkan eksepsi pada await Task.WhenAllbaris, memungkinkan penanganan eksepsi alami.
  2. Anda tahu bahwa tugas selesai di akhir metode ini, karena metode ini melakukan await Task.WhenAll. Jika Anda menggunakan async void, Anda tidak dapat dengan mudah mengetahui kapan operasi telah selesai.
  3. Pendekatan ini memiliki sintaks alami untuk mengambil hasil. GetAdminsFromGroupAsyncterdengar seperti itu adalah operasi yang menghasilkan hasil (admin), dan kode seperti itu lebih alami jika operasi semacam itu dapat mengembalikan hasilnya daripada menetapkan nilai sebagai efek samping.
Stephen Cleary
sumber
5
Bukan berarti itu mengubah apa pun, tetapi List.ForEach()bukan bagian dari LINQ.
svick
Saran bagus @StephenCleary dan terima kasih atas semua jawaban yang Anda berikan async. Mereka sangat membantu!
Justin Helgerson
4
@StewartAnderson: Tugas akan dijalankan secara bersamaan. Tidak ada ekstensi untuk eksekusi serial; cukup lakukan foreachdengan awaitdi tubuh lingkaran Anda.
Stephen Cleary
1
@mare: ForEachhanya membutuhkan jenis delegasi sinkron, dan tidak ada kelebihan beban saat mengambil jenis delegasi asinkron. Jadi jawaban singkatnya adalah "tidak ada yang menulis asynchronous ForEach". Jawaban yang lebih panjang adalah Anda harus mengasumsikan beberapa semantik; Misalnya, apakah barang harus diproses satu per satu (suka foreach), atau bersamaan (suka Select)? Jika satu per satu, bukankah aliran asinkron menjadi solusi yang lebih baik? Jika bersamaan, apakah hasilnya harus dalam urutan barang asli atau dalam urutan penyelesaian? Haruskah gagal pada kegagalan pertama atau menunggu sampai semua selesai? Dll
Stephen Cleary
2
@RogerWolf: Ya; digunakan SemaphoreSlimuntuk membatasi tugas-tugas asinkron.
Stephen Cleary
61

Metode ekstensi kecil ini akan memberi Anda iterasi asinkron yang aman-pengecualian:

public static async Task ForEachAsync<T>(this List<T> list, Func<T, Task> func)
{
    foreach (var value in list)
    {
        await func(value);
    }
}

Karena kita mengubah jenis kembalian lambda dari voidmenjadi Task, pengecualian akan menyebar dengan benar. Ini akan memungkinkan Anda untuk menulis sesuatu seperti ini dalam praktik:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});
JD Courtoy
sumber
Saya percaya asyncharus sebelumi =>
Todd
Daripada menunggu ForEachAsyn (), kita juga bisa memanggil Wait ().
Jonas
Lambda tidak perlu ditunggu di sini.
hazzik
Saya akan menambahkan dukungan untuk CancellationToken seperti dalam Jawaban Todd di sini stackoverflow.com/questions/29787098/…
Zorkind
Ini ForEachAsyncpada dasarnya adalah metode perpustakaan, jadi menunggu mungkin harus dikonfigurasi dengan ConfigureAwait(false).
Theodor Zoulias
9

Jawaban sederhananya adalah dengan menggunakan foreachkata kunci alih-alih ForEach()metode List().

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}
Bebek karet
sumber
Anda jenius
Vick_onrails
8

Berikut adalah versi yang berfungsi dari varian async foreach di atas dengan pemrosesan berurutan:

public static async Task ForEachAsync<T>(this List<T> enumerable, Action<T> action)
{
    foreach (var item in enumerable)
        await Task.Run(() => { action(item); }).ConfigureAwait(false);
}

Berikut implementasinya:

public async void SequentialAsync()
{
    var list = new List<Action>();

    Action action1 = () => {
        //do stuff 1
    };

    Action action2 = () => {
        //do stuff 2
    };

    list.Add(action1);
    list.Add(action2);

    await list.ForEachAsync();
}

Apa perbedaan utamanya? .ConfigureAwait(false);yang mempertahankan konteks utas utama sementara pemrosesan sekuensial asinkron untuk setiap tugas.

mrogunlana
sumber
6

Dimulai dengan C# 8.0, Anda dapat membuat dan menggunakan aliran secara asinkron.

    private async void button1_Click(object sender, EventArgs e)
    {
        IAsyncEnumerable<int> enumerable = GenerateSequence();

        await foreach (var i in enumerable)
        {
            Debug.WriteLine(i);
        }
    }

    public static async IAsyncEnumerable<int> GenerateSequence()
    {
        for (int i = 0; i < 20; i++)
        {
            await Task.Delay(100);
            yield return i;
        }
    }

Lebih

Andrei Krasutski
sumber
1
Keuntungannya, selain menunggu tiap elemen, kini juga menunggu MoveNextenumerator. Ini penting dalam kasus di mana enumerator tidak dapat mengambil elemen berikutnya secara instan, dan harus menunggu hingga salah satu elemen tersedia.
Theodor Zoulias
3

Tambahkan metode ekstensi ini

public static class ForEachAsyncExtension
{
    public static Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
    {
        return Task.WhenAll(from partition in Partitioner.Create(source).GetPartitions(dop) 
            select Task.Run(async delegate
            {
                using (partition)
                    while (partition.MoveNext())
                        await body(partition.Current).ConfigureAwait(false);
            }));
    }
}

Dan kemudian gunakan seperti ini:

Task.Run(async () =>
{
    var s3 = new AmazonS3Client(Config.Instance.Aws.Credentials, Config.Instance.Aws.RegionEndpoint);
    var buckets = await s3.ListBucketsAsync();

    foreach (var s3Bucket in buckets.Buckets)
    {
        if (s3Bucket.BucketName.StartsWith("mybucket-"))
        {
            log.Information("Bucket => {BucketName}", s3Bucket.BucketName);

            ListObjectsResponse objects;
            try
            {
                objects = await s3.ListObjectsAsync(s3Bucket.BucketName);
            }
            catch
            {
                log.Error("Error getting objects. Bucket => {BucketName}", s3Bucket.BucketName);
                continue;
            }

            // ForEachAsync (4 is how many tasks you want to run in parallel)
            await objects.S3Objects.ForEachAsync(4, async s3Object =>
            {
                try
                {
                    log.Information("Bucket => {BucketName} => {Key}", s3Bucket.BucketName, s3Object.Key);
                    await s3.DeleteObjectAsync(s3Bucket.BucketName, s3Object.Key);
                }
                catch
                {
                    log.Error("Error deleting bucket {BucketName} object {Key}", s3Bucket.BucketName, s3Object.Key);
                }
            });

            try
            {
                await s3.DeleteBucketAsync(s3Bucket.BucketName);
            }
            catch
            {
                log.Error("Error deleting bucket {BucketName}", s3Bucket.BucketName);
            }
        }
    }
}).Wait();
superlogical
sumber
2

Masalahnya adalah asynckata kunci harus muncul sebelum lambda, bukan sebelum isi:

db.Groups.ToList().ForEach(async (i) => {
    await GetAdminsFromGroup(i.Gid);
});
James Jeffery
sumber
35
-1 untuk penggunaan yang tidak perlu dan tidak kentara async void. Pendekatan ini memiliki masalah seputar penanganan pengecualian dan mengetahui kapan operasi asinkron selesai.
Stephen Cleary
Ya, menurut saya ini tidak menangani pengecualian dengan benar.
Herman Schoenfeld