ฉันจะใช้ Async กับ ForEach ได้อย่างไร?


123

เป็นไปได้ไหมที่จะใช้ Async เมื่อใช้ ForEach ด้านล่างนี้คือรหัสที่ฉันกำลังพยายาม:

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

ฉันได้รับข้อผิดพลาด:

ไม่มีชื่อ 'Async' ในบริบทปัจจุบัน

วิธีการที่ใช้คำสั่งอยู่ในนั้นถูกตั้งค่าเป็น async

คำตอบ:


180

List<T>.ForEachเล่นได้ไม่ดีเป็นพิเศษกับasync(LINQ-to-objects ไม่เหมือนกันด้วยเหตุผลเดียวกัน)

ในกรณีนี้ขอแนะนำให้ฉายแต่ละองค์ประกอบในการดำเนินการแบบอะซิงโครนัสจากนั้นคุณสามารถ (แบบอะซิงโครนัส) รอให้องค์ประกอบทั้งหมดเสร็จสมบูรณ์

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

ประโยชน์ของแนวทางนี้ในการasyncมอบอำนาจForEachคือ:

  1. การจัดการข้อผิดพลาดนั้นเหมาะสมกว่า ข้อยกเว้นจากasync voidไม่สามารถจับได้ด้วยcatch; วิธีนี้จะเผยแพร่ข้อยกเว้นที่await Task.WhenAllบรรทัดทำให้สามารถจัดการข้อยกเว้นได้ตามธรรมชาติ
  2. คุณรู้ว่างานจะเสร็จสมบูรณ์ในตอนท้ายของวิธีนี้เนื่องจากมันทำawait Task.WhenAllไฟล์. หากคุณใช้async voidคุณจะไม่สามารถบอกได้อย่างง่ายดายว่าการดำเนินการเสร็จสิ้นเมื่อใด
  3. แนวทางนี้มีไวยากรณ์ที่เป็นธรรมชาติสำหรับการดึงผลลัพธ์ GetAdminsFromGroupAsyncดูเหมือนว่าเป็นการดำเนินการที่สร้างผลลัพธ์ (ผู้ดูแลระบบ) และโค้ดดังกล่าวจะเป็นธรรมชาติมากขึ้นหากการดำเนินการดังกล่าวสามารถส่งคืนผลลัพธ์ได้แทนที่จะตั้งค่าเป็นผลข้างเคียง

5
ไม่ใช่ว่าจะเปลี่ยนแปลงอะไร แต่List.ForEach()ไม่ได้เป็นส่วนหนึ่งของ LINQ
svick

ข้อเสนอแนะที่ยอดเยี่ยม @StephenCleary และขอบคุณสำหรับคำตอบทั้งหมดที่คุณให้asyncไว้ พวกเขามีประโยชน์มาก!
Justin Helgerson

4
@StewartAnderson: งานจะดำเนินการพร้อมกัน ไม่มีส่วนขยายสำหรับการดำเนินการแบบอนุกรม เพียงแค่ทำforeachกับawaitในร่างกายห่วงของคุณ
Stephen Cleary

1
@mare: ForEachใช้ประเภทผู้รับมอบสิทธิ์แบบซิงโครนัสเท่านั้นและไม่มีการโอเวอร์โหลดสำหรับประเภทผู้ร่วมประชุมแบบอะซิงโครนัส ดังนั้นคำตอบสั้น ๆ คือ "ไม่มีใครเขียนอะซิงโครนัสForEach" คำตอบที่ยาวกว่าคือคุณต้องถือว่าความหมายบางอย่าง เช่นควรประมวลผลรายการทีละรายการ (เช่นforeach) หรือพร้อมกัน (เช่นSelect)? ถ้าทีละสตรีมแบบอะซิงโครนัสจะไม่ใช่ทางออกที่ดีกว่าหรือ หากพร้อมกันผลลัพธ์ควรอยู่ในลำดับรายการดั้งเดิมหรือตามลำดับความสำเร็จ? มันควรจะล้มเหลวในความล้มเหลวครั้งแรกหรือรอจนกว่าทั้งหมดจะเสร็จสมบูรณ์? ฯลฯ
Stephen Cleary

2
@RogerWolf: ครับ; ใช้SemaphoreSlimเพื่อเค้นงานแบบอะซิงโครนัส
Stephen Cleary

61

วิธีการขยายเล็กน้อยนี้ควรให้การทำซ้ำแบบไม่ซิงค์ที่ปลอดภัยเป็นพิเศษ:

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

เนื่องจากเรากำลังเปลี่ยนประเภทการส่งคืนของแลมด้าจากvoidเป็นTaskเป็นข้อยกเว้นจะเผยแพร่ได้อย่างถูกต้อง สิ่งนี้จะช่วยให้คุณสามารถเขียนสิ่งนี้ได้ในทางปฏิบัติ:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});

ฉันเชื่อว่าasyncควรจะมาก่อนi =>
ทอดด์

แทนที่จะรอ ForEachAsyn () เราสามารถเรียก Wait () ได้
Jonas

แลมด้าไม่ต้องรอที่นี่
hazzik

ฉันจะเพิ่มการรองรับ
Can CancelToken

โดยForEachAsyncพื้นฐานแล้วเป็นวิธีการไลบรารีดังนั้นการรอคอยควรได้รับการกำหนดค่าด้วยConfigureAwait(false).
Theodor Zoulias

9

คำตอบง่ายๆคือการใช้foreachคำหลักแทนการใช้วิธีการของForEach()List()

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}

คุณเป็นอัจฉริยะ
Vick_onrails

8

นี่คือเวอร์ชันที่ใช้งานได้จริงของตัวแปร async foreach ข้างต้นพร้อมการประมวลผลตามลำดับ:

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

นี่คือการนำไปใช้:

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

อะไรคือความแตกต่างที่สำคัญ? .ConfigureAwait(false);ซึ่งจะเก็บบริบทของเธรดหลักในขณะที่การประมวลผลตามลำดับแบบ async ของแต่ละงาน


6

เริ่มต้นด้วยC# 8.0คุณสามารถสร้างและใช้สตรีมแบบอะซิงโครนัสได้

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

มากกว่า


1
สิ่งนี้มีข้อดีที่นอกเหนือจากการรอแต่ละองค์ประกอบแล้วตอนนี้คุณยังรอMoveNextการแจงนับ นี่เป็นสิ่งสำคัญในกรณีที่ตัวแจงนับไม่สามารถดึงองค์ประกอบถัดไปได้ในทันทีและต้องรอให้พร้อมใช้งาน
Theodor Zoulias

3

เพิ่มวิธีการขยายนี้

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

จากนั้นใช้ดังนี้:

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

2

ปัญหาคือasyncคีย์เวิร์ดต้องปรากฏต่อหน้าแลมบ์ดาไม่ใช่ข้างหน้าเนื้อหา:

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

35
-1 async voidสำหรับการใช้งานที่ไม่จำเป็นและลึกซึ้งของ แนวทางนี้มีปัญหาเกี่ยวกับการจัดการข้อยกเว้นและการทราบเมื่อการดำเนินการแบบอะซิงโครนัสเสร็จสมบูรณ์
Stephen Cleary

ใช่ฉันพบว่าสิ่งนี้ไม่จัดการข้อยกเว้นอย่างถูกต้อง
Herman Schoenfeld
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.