Làm cách nào để sử dụng Async với ForEach?


123

Có thể sử dụng Async khi sử dụng ForEach không? Dưới đây là mã tôi đang thử:

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

Tôi gặp lỗi:

Tên 'Async' không tồn tại trong ngữ cảnh hiện tại

Phương thức mà câu lệnh using đi kèm được đặt thành không đồng bộ.

Câu trả lời:


180

List<T>.ForEachkhông hoạt động đặc biệt tốt với async(LINQ-to-object cũng vậy, vì những lý do tương tự).

Trong trường hợp này, tôi khuyên bạn nên chiếu từng phần tử vào một hoạt động không đồng bộ và sau đó bạn có thể (không đồng bộ) đợi tất cả chúng hoàn thành.

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

Các lợi ích của cách tiếp cận này so với việc giao cho một asyncđại biểu ForEachlà:

  1. Xử lý lỗi đúng cách hơn. async voidKhông thể bắt được các trường hợp ngoại lệ từ catch; cách tiếp cận này sẽ tuyên truyền các ngoại lệ tại await Task.WhenAlldòng, cho phép xử lý ngoại lệ tự nhiên.
  2. Bạn biết rằng các nhiệm vụ đã hoàn thành ở cuối phương pháp này, vì nó thực hiện một await Task.WhenAll. Nếu bạn sử dụng async void, bạn không thể dễ dàng biết khi nào các hoạt động đã hoàn thành.
  3. Cách tiếp cận này có một cú pháp tự nhiên để lấy kết quả. GetAdminsFromGroupAsyncnghe có vẻ như đó là một hoạt động tạo ra một kết quả (các quản trị viên) và mã như vậy sẽ tự nhiên hơn nếu các hoạt động như vậy có thể trả về kết quả của chúng thay vì đặt một giá trị như một tác dụng phụ.

5
Không phải nó thay đổi bất cứ điều gì, nhưng List.ForEach()không phải là một phần của LINQ.
svick

Đề xuất tuyệt vời @StephenCleary và cảm ơn bạn vì tất cả các câu trả lời bạn đã đưa ra async. Họ từng rất hay giúp đỡ người khác!
Justin Helgerson

4
@StewartAnderson: Các tác vụ sẽ thực hiện đồng thời. Không có phần mở rộng để thực hiện nối tiếp; chỉ cần làm foreachvới một awaittrong phần thân vòng lặp của bạn.
Stephen Cleary

1
@mare: ForEachchỉ nhận loại đại biểu đồng bộ và không có quá tải khi lấy loại đại biểu không đồng bộ. Vì vậy, câu trả lời ngắn gọn là "không ai đã viết một không đồng bộ ForEach". Câu trả lời dài hơn là bạn phải giả định một số ngữ nghĩa; ví dụ: nên xử lý từng mục một (như foreach) hay đồng thời (như Select)? Nếu mỗi lần một luồng không đồng bộ có phải là giải pháp tốt hơn không? Nếu đồng thời, các kết quả nên theo thứ tự mục ban đầu hay theo thứ tự hoàn thành? Nó nên thất bại trong lần thất bại đầu tiên hay đợi cho đến khi tất cả đã hoàn thành? Vv
Stephen Cleary.

2
@RogerWolf: Có; sử dụng SemaphoreSlimđể điều chỉnh các tác vụ không đồng bộ.
Stephen Cleary

61

Phương thức mở rộng nhỏ này sẽ cung cấp cho bạn phép lặp không đồng bộ ngoại lệ an toàn:

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

Vì chúng tôi đang thay đổi kiểu trả về của lambda từ voidthành Task, các ngoại lệ sẽ phổ biến chính xác. Điều này sẽ cho phép bạn viết một cái gì đó như thế này trong thực tế:

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

Tôi tin rằng asyncnên có trướci =>
Todd

Thay vì chờ ForEachAsyn (), người ta cũng có thể gọi Wait ().
Jonas

Lambda không cần phải chờ đợi ở đây.
hazzik

Tôi sẽ thêm hỗ trợ cho
CancelToken

Về ForEachAsynccơ bản, phương thức này là một phương thức thư viện, vì vậy việc chờ đợi có thể được cấu hình với ConfigureAwait(false).
Theodor Zoulias

9

Câu trả lời đơn giản là sử dụng foreachtừ khóa thay vì ForEach()phương thức của List().

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

Bạn là một thiên tài
Vick_onrails

8

Đây là phiên bản hoạt động thực tế của các biến thể foreach không đồng bộ ở trên với xử lý tuần tự:

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

Đây là cách thực hiện:

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

Điểm khác biệt chính là gì? .ConfigureAwait(false);giữ bối cảnh của luồng chính trong khi xử lý tuần tự không đồng bộ của từng tác vụ.


6

Bắt đầu với C# 8.0, bạn có thể tạo và sử dụng luồng không đồng bộ.

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

Hơn


1
Điều này có ưu điểm là ngoài việc chờ đợi từng yếu tố, giờ đây bạn cũng đang chờ đợi MoveNextđiều tra viên. Điều này rất quan trọng trong trường hợp người điều tra không thể tìm nạp phần tử tiếp theo ngay lập tức và phải đợi một phần tử khả dụng.
Theodor Zoulias

3

Thêm phương thức tiện ích mở rộng này

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

Và sau đó sử dụng như vậy:

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

Vấn đề là asynctừ khóa cần xuất hiện trước lambda, không phải trước nội dung:

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

35
-1 để sử dụng không cần thiết và tinh vi async void. Cách tiếp cận này có các vấn đề xung quanh việc xử lý ngoại lệ và biết khi nào các hoạt động không đồng bộ hoàn thành.
Stephen Cleary

Có, tôi thấy điều này không xử lý đúng các trường hợp ngoại lệ.
Herman Schoenfeld
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.