Nghiên cứu song song với lambda không đồng bộ


138

Tôi muốn xử lý một bộ sưu tập song song, nhưng tôi gặp khó khăn khi thực hiện nó và do đó tôi hy vọng có được sự giúp đỡ.

Rắc rối phát sinh nếu tôi muốn gọi một phương thức được đánh dấu không đồng bộ trong C #, trong lambda của vòng lặp song song. Ví dụ:

var bag = new ConcurrentBag<object>();
Parallel.ForEach(myCollection, async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
}
var count = bag.Count;

Vấn đề xảy ra với số đếm là 0, bởi vì tất cả các luồng được tạo ra thực sự chỉ là các luồng nền và Parallel.ForEachcuộc gọi không chờ hoàn thành. Nếu tôi xóa từ khóa async, phương thức sẽ như sau:

var bag = new ConcurrentBag<object>();
Parallel.ForEach(myCollection, item =>
{
  // some pre stuff
  var responseTask = await GetData(item);
  responseTask.Wait();
  var response = responseTask.Result;
  bag.Add(response);
  // some post stuff
}
var count = bag.Count;

Nó hoạt động, nhưng nó hoàn toàn vô hiệu hóa sự thông minh đang chờ đợi và tôi phải thực hiện một số xử lý ngoại lệ thủ công .. (Đã xóa để cho ngắn gọn).

Làm cách nào tôi có thể thực hiện một Parallel.ForEachvòng lặp, sử dụng từ khóa đang chờ trong lambda? Có thể không?

Nguyên mẫu của phương thức Parallel.ForEach có một Action<T> tham số làm tham số, nhưng tôi muốn nó chờ lambda không đồng bộ của tôi.


1
Tôi giả sử bạn có nghĩa là loại bỏ awaitkhỏi await GetData(item)khối mã thứ hai vì nó sẽ tạo ra lỗi biên dịch.
Josh M.

Câu trả lời:


186

Nếu bạn chỉ muốn song song đơn giản, bạn có thể làm điều này:

var bag = new ConcurrentBag<object>();
var tasks = myCollection.Select(async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
});
await Task.WhenAll(tasks);
var count = bag.Count;

Nếu bạn cần một cái gì đó phức tạp hơn, hãy xem bài viết của Stephen ToubForEachAsync .


46
Có lẽ một cơ chế tiết lưu là cần thiết. Điều này sẽ ngay lập tức tạo ra nhiều tác vụ như có các mục có thể kết thúc trong 10k yêu cầu mạng và như vậy.
usr

10
@usr Ví dụ cuối cùng trong bài viết của Stephen Toub đề cập đến điều đó.
Svick

@svick Mình khó hiểu về mẫu cuối cùng đó. Đối với tôi, nó chỉ tạo ra một loạt các nhiệm vụ để tạo ra nhiều nhiệm vụ hơn cho tôi, nhưng tất cả chúng đều bắt đầu thực hiện hàng loạt.
Luke Puplett

2
@LukePuplett Nó tạo ra dopcác tác vụ và mỗi tác vụ sau đó xử lý một số tập hợp con của bộ sưu tập đầu vào theo chuỗi.
Svick

4
@Afshin_Zavvar: Nếu bạn gọi Task.Runmà không awaitnhận được kết quả, thì đó chỉ là ném công việc cháy và quên vào nhóm luồng. Đó gần như luôn là một sai lầm.
Stephen Cleary

74

Bạn có thể sử dụng ParallelForEachAsyncphương thức tiện ích mở rộng từ Gói NuGet của AsyncEnumerator :

using Dasync.Collections;

var bag = new ConcurrentBag<object>();
await myCollection.ParallelForEachAsync(async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
}, maxDegreeOfParallelism: 10);
var count = bag.Count;

1
Đây là gói của bạn? Tôi đã thấy bạn đăng bài này ở một vài nơi bây giờ? : D Oh chờ đợi .. tên của bạn là trên gói: D +1
Piotr Kula

17
@ppumkin, vâng, nó là của tôi. Tôi đã gặp vấn đề này nhiều lần, vì vậy quyết định giải quyết nó theo cách đơn giản nhất có thể và giải phóng những người khác khỏi phải vật lộn :)
Serge Semenov

Cảm ơn .. nó chắc chắn có ý nghĩa và giúp tôi ra thời gian lớn!
Piotr Kula

2
bạn có một lỗi đánh máy: maxDegreeOfParallelism>maxDegreeOfParalellism
Shiran Dror

3
Viết đúng chính tả là maxDegreeOfParallelism, tuy nhiên có một điều gì đó trong nhận xét của @ ShiranDror - trong gói của bạn, bạn đã gọi biến maxDegreeOfParalellism do nhầm lẫn (và do đó mã được trích dẫn của bạn sẽ không được biên dịch cho đến khi bạn thay đổi nó ..)
Sinh

17

Với SemaphoreSlimbạn có thể đạt được kiểm soát song song.

var bag = new ConcurrentBag<object>();
var maxParallel = 20;
var throttler = new SemaphoreSlim(initialCount: maxParallel);
var tasks = myCollection.Select(async item =>
{
  try
  {
     await throttler.WaitAsync();
     var response = await GetData(item);
     bag.Add(response);
  }
  finally
  {
     throttler.Release();
  }
});
await Task.WhenAll(tasks);
var count = bag.Count;

3

Việc triển khai ParallelForEach không đồng bộ của tôi.

Đặc trưng:

  1. Throttling (mức độ song song tối đa).
  2. Xử lý ngoại lệ (ngoại lệ tổng hợp sẽ được ném khi hoàn thành).
  3. Bộ nhớ hiệu quả (không cần lưu trữ danh sách các tác vụ).

public static class AsyncEx
{
    public static async Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> asyncAction, int maxDegreeOfParallelism = 10)
    {
        var semaphoreSlim = new SemaphoreSlim(maxDegreeOfParallelism);
        var tcs = new TaskCompletionSource<object>();
        var exceptions = new ConcurrentBag<Exception>();
        bool addingCompleted = false;

        foreach (T item in source)
        {
            await semaphoreSlim.WaitAsync();
            asyncAction(item).ContinueWith(t =>
            {
                semaphoreSlim.Release();

                if (t.Exception != null)
                {
                    exceptions.Add(t.Exception);
                }

                if (Volatile.Read(ref addingCompleted) && semaphoreSlim.CurrentCount == maxDegreeOfParallelism)
                {
                    tcs.SetResult(null);
                }
            });
        }

        Volatile.Write(ref addingCompleted, true);
        await tcs.Task;
        if (exceptions.Count > 0)
        {
            throw new AggregateException(exceptions);
        }
    }
}

Ví dụ sử dụng:

await Enumerable.Range(1, 10000).ParallelForEachAsync(async (i) =>
{
    var data = await GetData(i);
}, maxDegreeOfParallelism: 100);

2

Tôi đã tạo một phương thức mở rộng cho phương thức này sử dụng SemaphoreSlim và cũng cho phép đặt mức độ song song tối đa

    /// <summary>
    /// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">Type of IEnumerable</typeparam>
    /// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
    /// <param name="action">an async <see cref="Action" /> to execute</param>
    /// <param name="maxDegreeOfParallelism">Optional, An integer that represents the maximum degree of parallelism,
    /// Must be grater than 0</param>
    /// <returns>A Task representing an async operation</returns>
    /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
    public static async Task ForEachAsyncConcurrent<T>(
        this IEnumerable<T> enumerable,
        Func<T, Task> action,
        int? maxDegreeOfParallelism = null)
    {
        if (maxDegreeOfParallelism.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxDegreeOfParallelism.Value, maxDegreeOfParallelism.Value))
            {
                var tasksWithThrottler = new List<Task>();

                foreach (var item in enumerable)
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await semaphoreSlim.WaitAsync();

                    tasksWithThrottler.Add(Task.Run(async () =>
                    {
                        await action(item).ContinueWith(res =>
                        {
                            // action is completed, so decrement the number of currently running tasks
                            semaphoreSlim.Release();
                        });
                    }));
                }

                // Wait for all tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

Sử dụng mẫu:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);

'sử dụng' sẽ không giúp đỡ. vòng lặp foreach sẽ chờ semaphone vô thời hạn. Chỉ cần thử mã đơn giản này để tái tạo vấn đề: đang chờ Enumerable.Range (1, 4) .ororachachyncync 2);
nicolay.anykienko

@ nicolay.anykienko bạn nói đúng về # 2. Vấn đề bộ nhớ đó có thể được giải quyết bằng cách thêm taskWithThrottler.Remove ALL (x => x.IsCompleted);
hỏi

1
Tôi đã thử nó trong mã của tôi và nếu tôi maxDegreeOfParallelism không null thì bế tắc mã. Tại đây bạn có thể thấy tất cả các mã để sao chép: stackoverflow.com/questions/58793118/ trên
Massimo Savazzi
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.