Nesting đang chờ trong Parallel.ForEach


183

Trong một ứng dụng tàu điện ngầm, tôi cần thực hiện một số cuộc gọi WCF. Có một số lượng đáng kể các cuộc gọi được thực hiện, vì vậy tôi cần thực hiện chúng trong một vòng lặp song song. Vấn đề là vòng lặp song song thoát trước khi các cuộc gọi WCF hoàn tất.

Làm thế nào bạn sẽ tái cấu trúc này để làm việc như mong đợi?

var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
var customers = new  System.Collections.Concurrent.BlockingCollection<Customer>();

Parallel.ForEach(ids, async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var cust = await repo.GetCustomer(i);
    customers.Add(cust);
});

foreach ( var customer in customers )
{
    Console.WriteLine(customer.ID);
}

Console.ReadKey();

Câu trả lời:


172

Toàn bộ ý tưởng đằng sau Parallel.ForEach()là bạn có một tập hợp các luồng và mỗi luồng xử lý một phần của bộ sưu tập. Như bạn nhận thấy, điều này không hoạt động với async- await, nơi bạn muốn giải phóng chuỗi trong suốt thời gian của cuộc gọi không đồng bộ.

Bạn có thể khắc phục lỗi đó bằng cách chặn các ForEach()luồng, nhưng điều đó đánh bại toàn bộ quan điểm của async- await.

Những gì bạn có thể làm là sử dụng TPL Dataflow thay vì Parallel.ForEach(), hỗ trợ tốt cho sự không đồng bộ Task.

Cụ thể, mã của bạn có thể được viết bằng cách sử dụng TransformBlockbiến đổi từng id thành Customersử dụng asynclambda. Khối này có thể được cấu hình để thực hiện song song. Bạn sẽ liên kết khối đó với một khối ActionBlockghi mỗi Customerbảng điều khiển. Sau khi bạn thiết lập mạng chặn, bạn có thể Post()mỗi id thành TransformBlock.

Trong mã:

var ids = new List<string> { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };

var getCustomerBlock = new TransformBlock<string, Customer>(
    async i =>
    {
        ICustomerRepo repo = new CustomerRepo();
        return await repo.GetCustomer(i);
    }, new ExecutionDataflowBlockOptions
    {
        MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded
    });
var writeCustomerBlock = new ActionBlock<Customer>(c => Console.WriteLine(c.ID));
getCustomerBlock.LinkTo(
    writeCustomerBlock, new DataflowLinkOptions
    {
        PropagateCompletion = true
    });

foreach (var id in ids)
    getCustomerBlock.Post(id);

getCustomerBlock.Complete();
writeCustomerBlock.Completion.Wait();

Mặc dù bạn có thể muốn hạn chế sự song song của TransformBlockmột số hằng số nhỏ. Ngoài ra, bạn có thể giới hạn dung lượng của TransformBlockvà thêm các mục vào nó không đồng bộ bằng cách sử dụng SendAsync(), ví dụ nếu bộ sưu tập quá lớn.

Như một lợi ích bổ sung khi so sánh với mã của bạn (nếu nó hoạt động) là việc viết sẽ bắt đầu ngay khi một mục duy nhất kết thúc và không đợi cho đến khi tất cả quá trình xử lý kết thúc.


2
Tổng quan rất ngắn gọn về async, tiện ích mở rộng phản ứng, TPL và TPL DataFlow - vantsuyoshi.wordpress.com/2012/01/05/ cho những người như tôi có thể cần một sự rõ ràng.
Norman H

1
Tôi khá chắc chắn rằng câu trả lời này KHÔNG song song với việc xử lý. Tôi tin rằng bạn cần phải thực hiện Parallel.ForEach trên các id và đăng chúng lên getCustomerBlock. Ít nhất đó là những gì tôi tìm thấy khi tôi thử nghiệm đề xuất này.
JasonLind

4
@JasonLind Nó thực sự làm. Sử dụng Parallel.ForEach()để Post()mục song song không nên có bất kỳ tác dụng thực sự.
svick

1
@svick Ok tôi đã tìm thấy nó, ActionBlock cũng cần phải song song. Tôi đã làm nó hơi khác một chút, tôi không cần chuyển đổi nên tôi chỉ sử dụng bộ đệm và thực hiện công việc của mình trong ActionBlock. Tôi đã nhầm lẫn từ một câu trả lời khác trên các interwebs.
JasonLind

2
Theo đó, ý tôi là chỉ định MaxDegreeOfParallelism trên ActionBlock giống như bạn làm trên TransformBlock trong ví dụ của bạn
JasonLind

125

Câu trả lời của Svick là (như thường lệ) xuất sắc.

Tuy nhiên, tôi thấy Dataflow hữu ích hơn khi bạn thực sự có lượng lớn dữ liệu cần truyền. Hoặc khi bạn cần một asynchàng đợi tương thích.

Trong trường hợp của bạn, một giải pháp đơn giản hơn là chỉ sử dụng asyncsong song kiểu:

var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };

var customerTasks = ids.Select(i =>
  {
    ICustomerRepo repo = new CustomerRepo();
    return repo.GetCustomer(i);
  });
var customers = await Task.WhenAll(customerTasks);

foreach (var customer in customers)
{
  Console.WriteLine(customer.ID);
}

Console.ReadKey();

13
Nếu bạn muốn hạn chế song song thủ công (điều mà bạn rất có thể làm trong trường hợp này), thực hiện theo cách này sẽ phức tạp hơn.
Svick

1
Nhưng bạn nói đúng rằng Dataflow có thể khá phức tạp (ví dụ khi so sánh với Parallel.ForEach()). Nhưng tôi nghĩ hiện tại nó là lựa chọn tốt nhất để thực hiện hầu hết mọi asynccông việc với các bộ sưu tập.
Svick

1
@JamesQuản lý ParallelOptionssẽ giúp như thế nào? Nó chỉ áp dụng cho Parallel.For/ForEach/Invoke, mà OP thành lập không được sử dụng ở đây.
Ohad Schneider

1
@StephenCleary Nếu GetCustomerphương thức đang trả về a Task<T>, có nên sử dụng Select(async i => { await repo.GetCustomer(i);});không?
Shyju

5
@batmaci: Parallel.ForEachkhông hỗ trợ async.
Stephen Cleary

81

Sử dụng DataFlow như đề xuất của Svick có thể là quá mức cần thiết và câu trả lời của Stephen không cung cấp phương tiện để kiểm soát sự tương tranh của hoạt động. Tuy nhiên, điều đó có thể đạt được khá đơn giản:

public static async Task RunWithMaxDegreeOfConcurrency<T>(
     int maxDegreeOfConcurrency, IEnumerable<T> collection, Func<T, Task> taskFactory)
{
    var activeTasks = new List<Task>(maxDegreeOfConcurrency);
    foreach (var task in collection.Select(taskFactory))
    {
        activeTasks.Add(task);
        if (activeTasks.Count == maxDegreeOfConcurrency)
        {
            await Task.WhenAny(activeTasks.ToArray());
            //observe exceptions here
            activeTasks.RemoveAll(t => t.IsCompleted); 
        }
    }
    await Task.WhenAll(activeTasks.ToArray()).ContinueWith(t => 
    {
        //observe exceptions in a manner consistent with the above   
    });
}

Các ToArray()cuộc gọi có thể được tối ưu hóa bằng cách sử dụng một mảng thay vì danh sách và thay thế các tác vụ đã hoàn thành, nhưng tôi nghi ngờ nó sẽ tạo ra nhiều sự khác biệt trong hầu hết các kịch bản. Sử dụng mẫu theo câu hỏi của OP:

RunWithMaxDegreeOfConcurrency(10, ids, async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var cust = await repo.GetCustomer(i);
    customers.Add(cust);
});

EDIT Fellow SO user và TPL wiz Eli Arbel đã chỉ cho tôi một bài viết liên quan từ Stephen Toub . Như thường lệ, việc thực hiện của anh ta vừa thanh lịch vừa hiệu quả:

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).ContinueWith(t => 
                          {
                              //observe exceptions
                          });

        })); 
}

1
@RichardPierre thực sự quá tải việc Partitioner.Createsử dụng phân vùng chunk này, nó cung cấp các phần tử một cách linh hoạt cho các tác vụ khác nhau để kịch bản bạn mô tả sẽ không diễn ra. Cũng lưu ý rằng phân vùng tĩnh (được xác định trước) có thể nhanh hơn trong một số trường hợp do ít chi phí hơn (cụ thể là đồng bộ hóa). Để biết thêm thông tin, hãy xem: msdn.microsoft.com/en-us/l Library / dd997411 (v = vs.110) .aspx .
Ohad Schneider

1
@OhadSchneider Trong // quan sát các ngoại lệ, nếu điều đó ném ra một ngoại lệ, nó có nổi bong bóng với người gọi không? Ví dụ, nếu tôi muốn toàn bộ vô số dừng xử lý / thất bại nếu bất kỳ phần nào của nó không thành công?
Terry

3
@Terry nó sẽ tạo bong bóng cho người gọi theo nghĩa là tác vụ cao nhất (được tạo bởi Task.WhenAll) sẽ chứa ngoại lệ (bên trong một AggregateException) và do đó, nếu người gọi nói được sử dụng await, một ngoại lệ sẽ được ném vào trang web cuộc gọi. Tuy nhiên, Task.WhenAllvẫn sẽ đợi tất cả các tác vụ hoàn thành và GetPartitionssẽ tự động phân bổ các phần tử khi partition.MoveNextđược gọi cho đến khi không còn phần tử nào được xử lý. Điều này có nghĩa là trừ khi bạn thêm cơ chế của riêng mình để dừng quá trình xử lý (ví dụ CancellationToken) nó sẽ không tự xảy ra.
Ohad Schneider

1
@gibbocool Tôi vẫn không chắc mình theo dõi. Giả sử bạn có tổng cộng 7 nhiệm vụ, với các tham số bạn đã chỉ định trong nhận xét của mình. Hơn nữa, giả sử rằng lô đầu tiên nhận nhiệm vụ 5 giây không thường xuyên và ba nhiệm vụ 1 giây. Sau khoảng một giây, tác vụ 5 giây vẫn sẽ được thực thi trong khi ba tác vụ 1 giây sẽ kết thúc. Tại thời điểm này, ba tác vụ 1 giây còn lại sẽ bắt đầu thực thi (chúng sẽ được phân vùng cung cấp cho ba luồng "miễn phí").
Ohad Schneider

2
@MichaelFreidgeim bạn có thể làm một cái gì đó như var current = partition.Currenttrước await bodyvà sau đó sử dụng currenttrong phần tiếp theo ( ContinueWith(t => { ... }).
Ohad Schneider

43

Bạn có thể tiết kiệm nỗ lực với Gói NuGet AsyncEnumerator mới , đã không tồn tại 4 năm trước khi câu hỏi ban đầu được đăng. Nó cho phép bạn kiểm soát mức độ song song:

using System.Collections.Async;
...

await ids.ParallelForEachAsync(async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var cust = await repo.GetCustomer(i);
    customers.Add(cust);
},
maxDegreeOfParallelism: 10);

Tuyên bố miễn trừ trách nhiệm: Tôi là tác giả của thư viện AsyncEnumerator, là nguồn mở và được cấp phép theo MIT, và tôi đang đăng thông báo này chỉ để giúp cộng đồng.


11
Serge, bạn nên tiết lộ rằng bạn là một tác giả của thư viện
Michael Freidgeim

5
ok, thêm từ chối trách nhiệm. Tôi không tìm kiếm bất kỳ lợi ích nào từ việc quảng cáo nó, chỉ muốn giúp đỡ mọi người;)
Serge Semenov

Thư viện của bạn không tương thích với .NET Core.
Corniel Nobel

2
@CornielNobel, nó tương thích với .NET Core - mã nguồn trên GitHub có phạm vi kiểm tra cho cả .NET Framework và .NET Core.
Serge Semenov

1
@SergeSemenov Tôi đã sử dụng thư viện của bạn rất nhiều cho nó AsyncStreamsvà tôi phải nói rằng nó rất tuyệt vời. Không thể đề nghị thư viện này đủ.
WBuck

16

Gói Parallel.Foreachthành một Task.Run()và thay vì awaitsử dụng từ khóa[yourasyncmethod].Result

(bạn cần thực hiện tác vụ task.Run để không chặn luồng UI)

Một cái gì đó như thế này:

var yourForeachTask = Task.Run(() =>
        {
            Parallel.ForEach(ids, i =>
            {
                ICustomerRepo repo = new CustomerRepo();
                var cust = repo.GetCustomer(i).Result;
                customers.Add(cust);
            });
        });
await yourForeachTask;

3
Có vấn đề gì với điều này? Tôi đã làm nó chính xác như thế này. Hãy Parallel.ForEachthực hiện công việc song song, chặn cho đến khi tất cả được thực hiện, và sau đó đẩy toàn bộ vào một luồng nền để có một giao diện người dùng đáp ứng. Bất kỳ vấn đề với điều đó? Có thể đó là một chủ đề ngủ quá nhiều, nhưng đó là mã ngắn, dễ đọc.
ygoe

@LonelyPixel Vấn đề duy nhất của tôi là nó gọi Task.Runkhi nào TaskCompletionSourcethích hợp hơn.
Gusdor 30/03/2016

1
@Gusdor Tò mò - tại sao lại TaskCompletionSourcethích hơn?
Cá biển

@Seafish Một câu hỏi hay mà tôi ước mình có thể trả lời. Phải là một ngày khó khăn: D
Gusdor 13/07/2016

Chỉ cần một bản cập nhật ngắn. Bây giờ tôi đang tìm kiếm chính xác điều này, cuộn xuống để tìm giải pháp đơn giản nhất và tìm lại nhận xét của riêng tôi. Tôi đã sử dụng chính xác mã này và nó hoạt động như mong đợi. Nó chỉ giả định rằng có một phiên bản Đồng bộ hóa của các cuộc gọi Async gốc trong vòng lặp. awaitcó thể được di chuyển ở phía trước để lưu tên biến phụ.
ygoe

7

Điều này sẽ khá hiệu quả và dễ dàng hơn là làm cho toàn bộ TPL Dataflow hoạt động:

var customers = await ids.SelectAsync(async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    return await repo.GetCustomer(i);
});

...

public static async Task<IList<TResult>> SelectAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector, int maxDegreesOfParallelism = 4)
{
    var results = new List<TResult>();

    var activeTasks = new HashSet<Task<TResult>>();
    foreach (var item in source)
    {
        activeTasks.Add(selector(item));
        if (activeTasks.Count >= maxDegreesOfParallelism)
        {
            var completed = await Task.WhenAny(activeTasks);
            activeTasks.Remove(completed);
            results.Add(completed.Result);
        }
    }

    results.AddRange(await Task.WhenAll(activeTasks));
    return results;
}

Không nên sử dụng ví dụ sử dụng awaitnhư : var customers = await ids.SelectAsync(async i => { ... });?
Paccc

5

Tôi đến bữa tiệc muộn một chút nhưng bạn có thể muốn xem xét sử dụng GetAwaiter.GetResult () để chạy mã async của mình trong ngữ cảnh đồng bộ nhưng tương tự như dưới đây;

 Parallel.ForEach(ids, i =>
{
    ICustomerRepo repo = new CustomerRepo();
    // Run this in thread which Parallel library occupied.
    var cust = repo.GetCustomer(i).GetAwaiter().GetResult();
    customers.Add(cust);
});

5

Một phương thức mở rộng cho việ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);

5

Sau khi giới thiệu một loạt các phương thức trợ giúp, bạn sẽ có thể chạy các truy vấn song song với cú pháp đơn giản này:

const int DegreeOfParallelism = 10;
IEnumerable<double> result = await Enumerable.Range(0, 1000000)
    .Split(DegreeOfParallelism)
    .SelectManyAsync(async i => await CalculateAsync(i).ConfigureAwait(false))
    .ConfigureAwait(false);

Điều xảy ra ở đây là: chúng tôi chia bộ sưu tập nguồn thành 10 khối ( .Split(DegreeOfParallelism)), sau đó chạy 10 tác vụ, mỗi tác vụ xử lý từng mục của nó ( .SelectManyAsync(...)) và hợp nhất chúng lại thành một danh sách.

Đáng nói là có một cách tiếp cận đơn giản hơn:

double[] result2 = await Enumerable.Range(0, 1000000)
    .Select(async i => await CalculateAsync(i).ConfigureAwait(false))
    .WhenAll()
    .ConfigureAwait(false);

Nhưng nó cần phải đề phòng : nếu bạn có một bộ sưu tập nguồn quá lớn, nó sẽ lên lịch Taskcho mọi mục ngay lập tức, điều này có thể gây ra các lượt truy cập hiệu suất đáng kể.

Các phương thức mở rộng được sử dụng trong các ví dụ trên trông như sau:

public static class CollectionExtensions
{
    /// <summary>
    /// Splits collection into number of collections of nearly equal size.
    /// </summary>
    public static IEnumerable<List<T>> Split<T>(this IEnumerable<T> src, int slicesCount)
    {
        if (slicesCount <= 0) throw new ArgumentOutOfRangeException(nameof(slicesCount));

        List<T> source = src.ToList();
        var sourceIndex = 0;
        for (var targetIndex = 0; targetIndex < slicesCount; targetIndex++)
        {
            var list = new List<T>();
            int itemsLeft = source.Count - targetIndex;
            while (slicesCount * list.Count < itemsLeft)
            {
                list.Add(source[sourceIndex++]);
            }

            yield return list;
        }
    }

    /// <summary>
    /// Takes collection of collections, projects those in parallel and merges results.
    /// </summary>
    public static async Task<IEnumerable<TResult>> SelectManyAsync<T, TResult>(
        this IEnumerable<IEnumerable<T>> source,
        Func<T, Task<TResult>> func)
    {
        List<TResult>[] slices = await source
            .Select(async slice => await slice.SelectListAsync(func).ConfigureAwait(false))
            .WhenAll()
            .ConfigureAwait(false);
        return slices.SelectMany(s => s);
    }

    /// <summary>Runs selector and awaits results.</summary>
    public static async Task<List<TResult>> SelectListAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector)
    {
        List<TResult> result = new List<TResult>();
        foreach (TSource source1 in source)
        {
            TResult result1 = await selector(source1).ConfigureAwait(false);
            result.Add(result1);
        }
        return result;
    }

    /// <summary>Wraps tasks with Task.WhenAll.</summary>
    public static Task<TResult[]> WhenAll<TResult>(this IEnumerable<Task<TResult>> source)
    {
        return Task.WhenAll<TResult>(source);
    }
}
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.