Làm thế nào để hạn chế số lượng hoạt động I / O không đồng bộ đồng thời?


115
// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let's send HTTP requests to each of these URLs in parallel
urls.AsParallel().ForAll(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
});

Đây là vấn đề, nó bắt đầu hơn 1000 yêu cầu web đồng thời. Có cách nào dễ dàng để giới hạn số lượng đồng thời của các yêu cầu http không đồng bộ này không? Vì vậy, không quá 20 trang web được tải xuống bất kỳ lúc nào. Làm thế nào để làm điều đó một cách hiệu quả nhất?


2
Điều này khác với câu hỏi trước của bạn như thế nào?
svick

1
stackoverflow.com/questions/9290498/… Với tham số ParallelOptions.
Chris Disley

4
@ChrisDisley, điều này sẽ chỉ song song việc khởi chạy các yêu cầu.
tiêu

@svick nói đúng, nó khác nhau như thế nào? btw, tôi yêu các câu trả lời có stackoverflow.com/a/10802883/66372
eglasius

3
Bên cạnh đó HttpClientIDisposable, và bạn nên vứt bỏ nó, đặc biệt là khi bạn đang sử dụng 1000 của họ. HttpClientcó thể được sử dụng như một singleton cho nhiều yêu cầu.
Shimmy Weitzhandler

Câu trả lời:


161

Bạn chắc chắn có thể thực hiện việc này trong các phiên bản mới nhất của không đồng bộ cho .NET, sử dụng .NET 4.5 Beta. Bài đăng trước từ 'usr' chỉ đến một bài báo hay được viết bởi Stephen Toub, nhưng tin tức ít được công bố hơn là semaphore không đồng bộ thực sự đã đưa nó vào bản phát hành Beta của .NET 4.5

Nếu bạn nhìn vào SemaphoreSlimlớp yêu quý của chúng tôi (mà bạn nên sử dụng vì nó hoạt động tốt hơn so với lớp ban đầu Semaphore), nó hiện tự hào với WaitAsync(...)hàng loạt quá tải, với tất cả các đối số được mong đợi - khoảng thời gian chờ, mã thông báo hủy, tất cả những người bạn lên lịch thông thường của bạn: )

Stephen's cũng đã viết một bài đăng trên blog gần đây hơn về các tính năng mới của .NET 4.5 ra mắt với phiên bản beta, hãy xem Có gì mới cho Parallelism trong .NET 4.5 Beta .

Cuối cùng, đây là một số mã mẫu về cách sử dụng SemaphoreSlim để điều chỉnh phương thức async:

public async Task MyOuterMethod()
{
    // let's say there is a list of 1000+ URLs
    var urls = { "http://google.com", "http://yahoo.com", ... };

    // now let's send HTTP requests to each of these URLs in parallel
    var allTasks = new List<Task>();
    var throttler = new SemaphoreSlim(initialCount: 20);
    foreach (var url in urls)
    {
        // do an async wait until we can schedule again
        await throttler.WaitAsync();

        // using Task.Run(...) to run the lambda in its own parallel
        // flow on the threadpool
        allTasks.Add(
            Task.Run(async () =>
            {
                try
                {
                    var client = new HttpClient();
                    var html = await client.GetStringAsync(url);
                }
                finally
                {
                    throttler.Release();
                }
            }));
    }

    // won't get here until all urls have been put into tasks
    await Task.WhenAll(allTasks);

    // won't get here until all tasks have completed in some way
    // (either success or exception)
}

Cuối cùng, nhưng có lẽ đáng được đề cập là một giải pháp sử dụng lập lịch dựa trên TPL. Bạn có thể tạo các tác vụ có giới hạn ủy quyền trên TPL chưa được khởi động và cho phép bộ lập lịch tác vụ tùy chỉnh để giới hạn đồng thời. Trên thực tế, có một mẫu MSDN cho nó ở đây:

Xem thêm TaskScheduler .


3
song song. trước với một mức độ song song hạn chế có phải là một cách tiếp cận tốt hơn không? msdn.microsoft.com/en-us/library/…
GreyCloud

2
Tại sao bạn không vứt bỏ bạnHttpClient
Shimmy Weitzhandler

4
@GreyCloud: Parallel.ForEachhoạt động với mã đồng bộ. Điều này cho phép bạn gọi mã không đồng bộ.
Josh Noe

2
@TheMonarch bạn nhầm rồi . Bên cạnh đó, luôn luôn có một thói quen tốt để gói gọn tất cả IDisposablecác câu usinghoặc try-finallycâu lệnh và đảm bảo chúng được sử dụng.
Shimmy Weitzhandler

29
Với mức độ phổ biến của câu trả lời này, cần chỉ ra rằng HttpClient có thể và nên là một trường hợp chung duy nhất chứ không phải là một trường hợp cho mỗi yêu cầu.
Rupert Rawnsley

15

Nếu bạn có IEnumerable (ví dụ: chuỗi các URL) và bạn muốn thực hiện thao tác liên kết I / O với từng thứ này (tức là thực hiện yêu cầu http không đồng bộ) đồng thời VÀ tùy chọn, bạn cũng muốn đặt số lượng đồng thời tối đa Yêu cầu I / O trong thời gian thực, đây là cách bạn có thể thực hiện điều đó. Theo cách này, bạn không sử dụng thread pool et al, phương pháp này sử dụng semaphoreslim để kiểm soát các yêu cầu I / O đồng thời tối đa tương tự như một mẫu cửa sổ trượt, một yêu cầu hoàn thành, rời khỏi semaphore và yêu cầu tiếp theo được đưa vào.

cách sử dụng: chờ ForEachAsync (urlStrings, YourAsyncFunc, tùy chọnMaxDegreeOfConcurrency);

public static Task ForEachAsync<TIn>(
        IEnumerable<TIn> inputEnumerable,
        Func<TIn, Task> asyncProcessor,
        int? maxDegreeOfParallelism = null)
    {
        int maxAsyncThreadCount = maxDegreeOfParallelism ?? DefaultMaxDegreeOfParallelism;
        SemaphoreSlim throttler = new SemaphoreSlim(maxAsyncThreadCount, maxAsyncThreadCount);

        IEnumerable<Task> tasks = inputEnumerable.Select(async input =>
        {
            await throttler.WaitAsync().ConfigureAwait(false);
            try
            {
                await asyncProcessor(input).ConfigureAwait(false);
            }
            finally
            {
                throttler.Release();
            }
        });

        return Task.WhenAll(tasks);
    }


không, bạn không cần phải hủy bỏ SemaphoreSlim một cách rõ ràng trong việc triển khai và sử dụng này vì nó được sử dụng nội bộ bên trong phương thức và phương thức không truy cập thuộc tính AvailableWaitHandle của nó, trong trường hợp đó chúng tôi cần phải hủy bỏ hoặc bọc nó trong một khối đang sử dụng.
Dogu Arslan

1
Chỉ cần nghĩ đến các phương pháp hay nhất và bài học mà chúng tôi dạy cho người khác. A usingsẽ tốt.
AgentFire

Vâng, ví dụ này tôi có thể làm theo, nhưng hãy thử tìm ra cách tốt nhất để làm điều này, về cơ bản có một bộ điều chỉnh nhưng Func của tôi sẽ trả về một danh sách, mà cuối cùng tôi muốn có trong danh sách cuối cùng của tất cả được hoàn thành khi hoàn thành ... có thể yêu cầu bị khóa trong danh sách, bạn có đề xuất.
Seabizkit

bạn có thể cập nhật một chút phương thức để nó trả về danh sách các tác vụ thực tế và bạn đang chờ Task.WhenAll từ bên trong mã gọi của bạn. Sau khi Task.WhenAll hoàn tất, bạn có thể liệt kê từng công việc trong danh sách và thêm danh sách của nó vào danh sách cuối cùng. Thay đổi chữ ký phương thức thành 'public static IEnumerable <Task <TOut>> ForEachAsync <TIn, TOut> (IEnumerable <TIn> inputEnumerable, Func <TIn, Task <TOut>> asyncProcessor, int? MaxDegreeOfParallelism = null)'
Dogu Arslan

7

Thật không may, .NET Framework thiếu các bộ tổ hợp quan trọng nhất để sắp xếp các tác vụ không đồng bộ song song. Không có thứ như vậy được tích hợp sẵn.

Hãy nhìn vào lớp AsyncSemaphore được xây dựng bởi Stephen Toub đáng kính nhất. Những gì bạn muốn được gọi là semaphore, và bạn cần một phiên bản không đồng bộ của nó.


12
Lưu ý rằng "Thật không may, .NET Framework thiếu các bộ tổ hợp quan trọng nhất để sắp xếp các tác vụ không đồng bộ song song. Không có thứ nào như vậy được tích hợp sẵn." không còn đúng như .NET 4.5 Beta. SemaphoreSlim hiện cung cấp chức năng WaitAsync (...) :)
Theo Yaung

SemaphoreSlim (với các phương thức async mới của nó) nên được ưu tiên hơn AsyncSemphore, hay việc triển khai Toub vẫn có một số lợi thế?
Todd Menier

Theo tôi, loại tích hợp sẵn nên được ưu tiên hơn vì nó có khả năng được kiểm tra tốt và thiết kế tốt.
usr

4
Stephen đã thêm một nhận xét để trả lời câu hỏi trên bài đăng trên blog của anh ấy xác nhận rằng việc sử dụng SemaphoreSlim cho .NET 4.5 nói chung sẽ là cách tốt nhất.
jdasilva

7

Có rất nhiều cạm bẫy và việc sử dụng trực tiếp một semaphore có thể khó khăn trong các trường hợp lỗi, vì vậy tôi khuyên bạn nên sử dụng Gói AsyncEnumerator NuGet thay vì phát minh lại bánh xe:

// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let's send HTTP requests to each of these URLs in parallel
await urls.ParallelForEachAsync(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
}, maxDegreeOfParalellism: 20);

4

Ví dụ Theo Yaung rất hay, nhưng có một biến thể không có danh sách các nhiệm vụ đang chờ.

 class SomeChecker
 {
    private const int ThreadCount=20;
    private CountdownEvent _countdownEvent;
    private SemaphoreSlim _throttler;

    public Task Check(IList<string> urls)
    {
        _countdownEvent = new CountdownEvent(urls.Count);
        _throttler = new SemaphoreSlim(ThreadCount); 

        return Task.Run( // prevent UI thread lock
            async  () =>{
                foreach (var url in urls)
                {
                    // do an async wait until we can schedule again
                    await _throttler.WaitAsync();
                    ProccessUrl(url); // NOT await
                }
                //instead of await Task.WhenAll(allTasks);
                _countdownEvent.Wait();
            });
    }

    private async Task ProccessUrl(string url)
    {
        try
        {
            var page = await new WebClient()
                       .DownloadStringTaskAsync(new Uri(url)); 
            ProccessResult(page);
        }
        finally
        {
            _throttler.Release();
            _countdownEvent.Signal();
        }
    }

    private void ProccessResult(string page){/*....*/}
}

4
Lưu ý, có một mối nguy hiểm khi sử dụng phương pháp này - bất kỳ ngoại lệ nào xảy ra trong ProccessUrlhoặc các chức năng con của nó sẽ thực sự bị bỏ qua. Chúng sẽ được bắt vào Nhiệm vụ, nhưng không được liên kết trở lại với người gọi ban đầu của Check(...). Cá nhân tôi, đó là lý do tại sao tôi vẫn sử dụng Task và các chức năng tổ hợp của chúng như WhenAllWhenAny- để truyền lỗi tốt hơn. :)
Theo Yaung

3

SemaphoreSlim có thể rất hữu ích ở đây. Đây là phương pháp mở rộng mà tôi đã tạo.

    /// <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="maxActionsToRunInParallel">Optional, max numbers of the actions to run in parallel,
    /// 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? maxActionsToRunInParallel = null)
    {
        if (maxActionsToRunInParallel.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxActionsToRunInParallel.Value, maxActionsToRunInParallel.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 of the provided tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

Cách sử dụng mẫu:

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

0

Câu hỏi cũ, câu trả lời mới. @vitidev có một khối mã được sử dụng lại gần như nguyên vẹn trong một dự án mà tôi đã xem xét. Sau khi thảo luận với một vài đồng nghiệp, một người hỏi "Tại sao bạn không sử dụng các phương pháp TPL có sẵn?" ActionBlock trông giống như người chiến thắng ở đó. https://msdn.microsoft.com/en-us/library/hh194773(v=vs.110).aspx . Có thể cuối cùng sẽ không thay đổi bất kỳ mã hiện có nào nhưng chắc chắn sẽ xem xét áp dụng mã số này và sử dụng lại phương pháp hay nhất của ông Softy cho song song được điều chỉnh.


0

Đây là một giải pháp tận dụng bản chất lười biếng của LINQ. Nó tương đương về mặt chức năng với câu trả lời được chấp nhận ), nhưng sử dụng worker-task thay vì a SemaphoreSlim, theo cách này, làm giảm dung lượng bộ nhớ của toàn bộ hoạt động. Lúc đầu, hãy làm cho nó hoạt động mà không cần điều chỉnh. Bước đầu tiên là chuyển đổi các url của chúng tôi thành vô số tác vụ.

string[] urls =
{
    "https://stackoverflow.com",
    "https://superuser.com",
    "https://serverfault.com",
    "https://meta.stackexchange.com",
    // ...
};
var httpClient = new HttpClient();
var tasks = urls.Select(async (url) =>
{
    return (Url: url, Html: await httpClient.GetStringAsync(url));
});

Bước thứ hai là thực awaithiện đồng thời tất cả các tác vụ bằng Task.WhenAllphương pháp:

var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
    Console.WriteLine($"Url: {result.Url}, {result.Html.Length:#,0} chars");
}

Đầu ra:

Url: https://stackoverflow.com , 105.574 ký tự
Url: https://superuser.com , 126.953 ký tự
Url: https://serverfault.com , 125.963 ký tự
Url: https://meta.stackexchange.com , 185.276 ký tự
...

Việc triển khai của Microsoft hiện thực hóa Task.WhenAllngay lập tức dữ liệu được cung cấp vào một mảng, khiến tất cả các tác vụ bắt đầu cùng một lúc. Chúng tôi không muốn điều đó, bởi vì chúng tôi muốn giới hạn số lượng các hoạt động không đồng bộ đồng thời. Vì vậy, chúng ta sẽ cần phải triển khai một giải pháp thay thế WhenAllsẽ liệt kê một cách nhẹ nhàng và chậm rãi. Chúng tôi sẽ làm điều đó bằng cách tạo một số tác vụ công nhân (bằng với mức đồng thời mong muốn) và mỗi tác vụ công nhân sẽ liệt kê một tác vụ có thể liệt kê của chúng tôi tại một thời điểm, sử dụng khóa để đảm bảo rằng mỗi tác vụ url sẽ được xử lý chỉ bởi một công nhân-nhiệm vụ. Sau đó, chúng tôi awaitcho tất cả các tác vụ công nhân hoàn thành và cuối cùng chúng tôi trả về kết quả. Đây là cách thực hiện:

public static async Task<T[]> WhenAll<T>(IEnumerable<Task<T>> tasks,
    int concurrencyLevel)
{
    if (tasks is ICollection<Task<T>>) throw new ArgumentException(
        "The enumerable should not be materialized.", nameof(tasks));
    var locker = new object();
    var results = new List<T>();
    var failed = false;
    using (var enumerator = tasks.GetEnumerator())
    {
        var workerTasks = Enumerable.Range(0, concurrencyLevel)
        .Select(async _ =>
        {
            try
            {
                while (true)
                {
                    Task<T> task;
                    int index;
                    lock (locker)
                    {
                        if (failed) break;
                        if (!enumerator.MoveNext()) break;
                        task = enumerator.Current;
                        index = results.Count;
                        results.Add(default); // Reserve space in the list
                    }
                    var result = await task.ConfigureAwait(false);
                    lock (locker) results[index] = result;
                }
            }
            catch (Exception)
            {
                lock (locker) failed = true;
                throw;
            }
        }).ToArray();
        await Task.WhenAll(workerTasks).ConfigureAwait(false);
    }
    lock (locker) return results.ToArray();
}

... và đây là những gì chúng ta phải thay đổi trong mã ban đầu của mình, để đạt được điều chỉnh mong muốn:

var results = await WhenAll(tasks, concurrencyLevel: 2);

Có một sự khác biệt liên quan đến việc xử lý các trường hợp ngoại lệ. Bản gốc Task.WhenAllđợi tất cả các nhiệm vụ hoàn thành và tổng hợp tất cả các ngoại lệ. Việc thực hiện trên chấm dứt ngay sau khi hoàn thành nhiệm vụ bị lỗi đầu tiên.


AC # 8 triển khai trả về một IAsyncEnumerable<T>có thể được tìm thấy ở đây .
Theodor Zoulias

-1

Mặc dù 1000 tác vụ có thể được xếp hàng đợi rất nhanh, nhưng thư viện Nhiệm vụ song song chỉ có thể xử lý các tác vụ đồng thời bằng số lõi CPU trong máy. Điều đó có nghĩa là nếu bạn có một máy bốn lõi, chỉ có 4 tác vụ sẽ được thực thi tại một thời điểm nhất định (trừ khi bạn giảm MaxDegreeOfParallelism).


8
Đúng, nhưng điều đó không liên quan đến hoạt động I / O không đồng bộ. Đoạn mã trên sẽ kích hoạt hơn 1000 lượt tải xuống đồng thời ngay cả khi nó đang chạy trên một luồng duy nhất.
đau buồn vào

Không thấy awaittừ khóa trong đó. Loại bỏ điều đó sẽ giải quyết vấn đề, chính xác?
scottm

2
Thư viện chắc chắn có thể xử lý nhiều tác vụ đang chạy (với Runningtrạng thái) đồng thời hơn số lượng lõi. Điều này đặc biệt xảy ra với Nhiệm vụ ràng buộc I / O.
svick

@svick: vâng. Bạn có biết cách kiểm soát hiệu quả các tác vụ TPL đồng thời tối đa (không phải luồng) không?
đau buồn vào

-1

Tính toán song song nên được sử dụng để tăng tốc các hoạt động liên kết với CPU. Ở đây chúng ta đang nói về các hoạt động ràng buộc I / O. Triển khai của bạn phải hoàn toàn không đồng bộ , trừ khi bạn áp đảo lõi đơn bận rộn trên CPU đa lõi của mình.

CHỈNH SỬA Tôi thích đề xuất của usr để sử dụng một "semaphore không đồng bộ" ở đây.


Điểm tốt! Mặc dù mỗi tác vụ ở đây sẽ chứa mã không đồng bộ và mã đồng bộ (trang được tải xuống không đồng bộ sau đó được xử lý theo cách đồng bộ). Tôi đang cố gắng phân phối phần mã đồng bộ trên các CPU và đồng thời giới hạn số lượng hoạt động I / O không đồng bộ đồng thời.
đau buồn vào

Tại sao? Bởi vì việc khởi chạy hơn 1000 yêu cầu http đồng thời có thể không phải là nhiệm vụ phù hợp với dung lượng mạng của người dùng.
tiêu

Các phần mở rộng song song cũng có thể được sử dụng như một cách để ghép các hoạt động I / O mà không cần phải triển khai giải pháp không đồng bộ thuần túy theo cách thủ công. Điều mà tôi đồng ý có thể được coi là cẩu thả, nhưng miễn là bạn giữ giới hạn chặt chẽ về số lượng các hoạt động đồng thời, nó có thể sẽ không làm căng thẳng nhóm luồng quá nhiều.
Sean U

3
Tôi không nghĩ rằng câu trả lời này đang cung cấp một câu trả lời. Ở đây hoàn toàn không đồng bộ là chưa đủ: Chúng tôi thực sự muốn điều chỉnh các IO vật lý theo cách không bị chặn.
usr

1
Hmm .. không chắc tôi đồng ý ... khi làm việc trong một dự án lớn, nếu có quá nhiều nhà phát triển áp dụng quan điểm này, bạn sẽ bị chết đói mặc dù đóng góp riêng của mỗi nhà phát triển là không đủ để vượt qua mọi thứ. Cho rằng chỉ có một ThreadPool, ngay cả khi bạn đang đối xử với nó một cách bán kính ... nếu những người khác cũng làm như vậy, rắc rối có thể xảy ra. Vì vậy, tôi luôn khuyên bạn không nên chạy những thứ dài trong ThreadPool.
tiêu

-1

Sử dụng MaxDegreeOfParallelism, là một tùy chọn bạn có thể chỉ định trong Parallel.ForEach():

var options = new ParallelOptions { MaxDegreeOfParallelism = 20 };

Parallel.ForEach(urls, options,
    url =>
        {
            var client = new HttpClient();
            var html = client.GetStringAsync(url);
            // do stuff with html
        });

4
Tôi không nghĩ rằng điều này hiệu quả. GetStringAsync(url)có nghĩa là được gọi với await. Nếu bạn kiểm tra loại var html, nó là a Task<string>, không phải kết quả string.
Neal Ehardt

2
@NealEhardt là đúng. Parallel.ForEach(...)được dùng để chạy các khối mã đồng bộ song song (ví dụ trên các luồng khác nhau).
Theo Yaung

-1

Về cơ bản, bạn sẽ muốn tạo một Hành động hoặc Nhiệm vụ cho mỗi URL mà bạn muốn truy cập, đặt chúng vào một Danh sách, rồi xử lý danh sách đó, giới hạn số lượng có thể được xử lý song song.

Bài đăng trên blog của tôi cho thấy cách thực hiện điều này cả với Nhiệm vụ và Hành động, đồng thời cung cấp một dự án mẫu mà bạn có thể tải xuống và chạy để xem cả hai hoạt động.

Với các hành động

Nếu sử dụng Actions, bạn có thể sử dụng hàm .Net Parallel.Invoke tích hợp sẵn. Ở đây chúng tôi giới hạn nó chạy song song tối đa 20 luồng.

var listOfActions = new List<Action>();
foreach (var url in urls)
{
    var localUrl = url;
    // Note that we create the Task here, but do not start it.
    listOfTasks.Add(new Task(() => CallUrl(localUrl)));
}

var options = new ParallelOptions {MaxDegreeOfParallelism = 20};
Parallel.Invoke(options, listOfActions.ToArray());

Với Nhiệm vụ

Với Task không có chức năng cài sẵn. Tuy nhiên, bạn có thể sử dụng cái mà tôi cung cấp trên blog của mình.

    /// <summary>
    /// Starts the given tasks and waits for them to complete. This will run, at most, the specified number of tasks in parallel.
    /// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
    /// </summary>
    /// <param name="tasksToRun">The tasks to run.</param>
    /// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    public static async Task StartAndWaitAllThrottledAsync(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, CancellationToken cancellationToken = new CancellationToken())
    {
        await StartAndWaitAllThrottledAsync(tasksToRun, maxTasksToRunInParallel, -1, cancellationToken);
    }

    /// <summary>
    /// Starts the given tasks and waits for them to complete. This will run the specified number of tasks in parallel.
    /// <para>NOTE: If a timeout is reached before the Task completes, another Task may be started, potentially running more than the specified maximum allowed.</para>
    /// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
    /// </summary>
    /// <param name="tasksToRun">The tasks to run.</param>
    /// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
    /// <param name="timeoutInMilliseconds">The maximum milliseconds we should allow the max tasks to run in parallel before allowing another task to start. Specify -1 to wait indefinitely.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    public static async Task StartAndWaitAllThrottledAsync(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, int timeoutInMilliseconds, CancellationToken cancellationToken = new CancellationToken())
    {
        // Convert to a list of tasks so that we don't enumerate over it multiple times needlessly.
        var tasks = tasksToRun.ToList();

        using (var throttler = new SemaphoreSlim(maxTasksToRunInParallel))
        {
            var postTaskTasks = new List<Task>();

            // Have each task notify the throttler when it completes so that it decrements the number of tasks currently running.
            tasks.ForEach(t => postTaskTasks.Add(t.ContinueWith(tsk => throttler.Release())));

            // Start running each task.
            foreach (var task in tasks)
            {
                // Increment the number of tasks currently running and wait if too many are running.
                await throttler.WaitAsync(timeoutInMilliseconds, cancellationToken);

                cancellationToken.ThrowIfCancellationRequested();
                task.Start();
            }

            // Wait for all of the provided tasks to complete.
            // We wait on the list of "post" tasks instead of the original tasks, otherwise there is a potential race condition where the throttler's using block is exited before some Tasks have had their "post" action completed, which references the throttler, resulting in an exception due to accessing a disposed object.
            await Task.WhenAll(postTaskTasks.ToArray());
        }
    }

Và sau đó tạo danh sách Nhiệm vụ của bạn và gọi hàm để chúng chạy, giả sử tối đa 20 công việc cùng một lúc, bạn có thể thực hiện điều này:

var listOfTasks = new List<Task>();
foreach (var url in urls)
{
    var localUrl = url;
    // Note that we create the Task here, but do not start it.
    listOfTasks.Add(new Task(async () => await CallUrl(localUrl)));
}
await Tasks.StartAndWaitAllThrottledAsync(listOfTasks, 20);

Tôi nghĩ rằng bạn chỉ đang chỉ định InitialCount cho SemaphoreSlim và bạn cần chỉ định tham số thứ 2 tức là maxCount trong hàm tạo của SemaphoreSlim.
Jay Shah

Tôi muốn mỗi phản hồi từ mỗi nhiệm vụ được xử lý thành một Danh sách. Làm cách nào để nhận được trả lại Kết quả hoặc phản hồi
venkat

-1

điều này không tốt vì nó thay đổi một biến toàn cục. nó cũng không phải là một giải pháp chung cho async. nhưng nó rất dễ dàng cho tất cả các phiên bản của HttpClient, nếu đó là tất cả những gì bạn muốn. bạn chỉ cần thử:

System.Net.ServicePointManager.DefaultConnectionLimit = 20;
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.