동시 비동기 I / O 작업의 양을 제한하는 방법은 무엇입니까?


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

여기에 문제가 있습니다. 1000 개 이상의 동시 웹 요청을 시작합니다. 이러한 비동기 http 요청의 동시 양을 제한하는 쉬운 방법이 있습니까? 따라서 한 번에 20 개 이상의 웹 페이지가 다운로드되지 않도록합니다. 가장 효율적인 방법으로 수행하는 방법은 무엇입니까?


2
이전 질문 과 어떻게 다른 가요?
svick

1
stackoverflow.com/questions/9290498/… ParallelOptions 매개 변수를 사용합니다.
Chris Disley

4
@ChrisDisley, 이것은 요청 시작을 병렬화합니다.
지출 자

@svick이 맞아요, 어떻게 다른가요? BTW, 나는 대답을 사랑이 stackoverflow.com/a/10802883/66372
eglasius

3
게다가 HttpClient입니다 IDisposable, 당신은 당신이 그들의 1000을 사용하는거야 특히, 그것을 폐기해야한다. HttpClient여러 요청에 대해 싱글 톤으로 사용할 수 있습니다.
Shimmy Weitzhandler

답변:


161

.NET 4.5 베타를 사용하여 .NET 용 비동기 최신 버전에서 확실히이 작업을 수행 할 수 있습니다. 'usr'의 이전 게시물은 Stephen Toub가 작성한 좋은 기사를 가리 키지 만 덜 알려진 소식은 비동기 세마포어가 실제로 .NET 4.5의 베타 릴리스에 포함되었다는 것입니다.

우리가 사랑하는 SemaphoreSlim클래스 (원래보다 성능이 뛰어 나기 때문에 사용해야 함)를 살펴보면 Semaphore, 이제 WaitAsync(...)예상되는 모든 인수 (시간 초과 간격, 취소 토큰, 모든 일반적인 스케줄링 친구)와 함께 일련의 오버로드를 자랑합니다 . )

Stephen은 또한 베타와 함께 출시 된 새로운 .NET 4.5 기능에 대한 최신 블로그 게시물을 작성했습니다. What 's New for Parallelism in .NET 4.5 Beta를 참조하십시오 .

마지막으로, 비동기 메서드 조절에 SemaphoreSlim을 사용하는 방법에 대한 몇 가지 샘플 코드가 있습니다.

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

마지막으로 TPL 기반 스케줄링을 사용하는 솔루션을 언급 할 가치가있을 것입니다. 아직 시작되지 않은 TPL에서 위임 바인딩 작업을 생성하고 사용자 지정 작업 스케줄러가 동시성을 제한하도록 허용 할 수 있습니다. 실제로 여기에 MSDN 샘플이 있습니다.

TaskScheduler를 참조하십시오 .


3
병렬 처리가 제한된 parallel.foreach가 더 좋은 접근 방식이 아닙니까? msdn.microsoft.com/en-us/library/…
GreyCloud

2
왜 당신은 당신을 버리지 않는다HttpClient
흔들 Weitzhandler에게

4
@GreyCloud : Parallel.ForEach동기 코드로 작동합니다. 이를 통해 비동기 코드를 호출 할 수 있습니다.
조쉬 노

2
@TheMonarch 당신이 틀렸어 . 항상 모든 포장하는 좋은 습관이다 외에도 IDisposable에서들 using또는 try-finally문, 그들의 처분을 확신합니다.
Shimmy Weitzhandler

29
이 답변이 얼마나 인기가 있는지를 감안할 때 HttpClient는 요청 당 인스턴스가 아닌 단일 공통 인스턴스가 될 수 있고 있어야한다는 점을 지적 할 가치가 있습니다.
Rupert Rawnsley

15

IEnumerable (즉, URL 문자열)이 있고 이들 각각에 대해 I / O 바운드 작업을 동시에 수행하고 (즉, 비동기 http 요청 만들기) 선택적으로 최대 동시 수를 설정하려는 경우 실시간 I / O 요청을 수행하는 방법은 다음과 같습니다. 이렇게하면 스레드 풀 등을 사용하지 않고이 메서드는 세마포어 슬림을 사용하여 하나의 요청이 완료되고 세마포어를 떠나고 다음 요청이 들어오는 슬라이딩 창 패턴과 유사한 최대 동시 I / O 요청을 제어합니다.

사용법 : await ForEachAsync (urlStrings, YourAsyncFunc, optionalMaxDegreeOfConcurrency);

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


아니요, SemaphoreSlim은 메서드 내부에서 내부적으로 사용되며 메서드가 해당 AvailableWaitHandle 속성에 액세스하지 않으므로이 구현 및 사용에서 명시 적으로 처리 할 필요가 없습니다.이 경우 using 블록 내에서 처리하거나 래핑해야했습니다.
Dogu Arslan

1
우리가 다른 사람들에게 가르치는 모범 사례와 교훈 만 생각하면됩니다. A using는 좋을 것입니다.
AgentFire

이 예제를 따를 수는 있지만이를 수행하는 가장 좋은 방법은 기본적으로 스로틀 러가 있지만 내 Func는 목록을 반환합니다.이 목록은 완료되면 완료된 모든 최종 목록에 포함됩니다. 목록에 잠겨 있어야합니다. 제안 사항이 있습니까?
Seabizkit

메서드를 약간 업데이트하여 실제 작업 목록을 반환하고 호출 코드 내부에서 Task.WhenAll을 기다릴 수 있습니다. Task.WhenAll이 완료되면 목록의 각 작업을 열거하고 해당 목록을 최종 목록에 추가 할 수 있습니다. 메서드 서명을 'public static IEnumerable <Task <TOut >> ForEachAsync <TIn, TOut> (IEnumerable <TIn> inputEnumerable, Func <TIn, Task <TOut >> asyncProcessor, int? maxDegreeOfParallelism = null)'로 변경합니다.
Dogu Arslan

7

불행히도 .NET Framework에는 병렬 비동기 작업을 조정하기위한 가장 중요한 결합자가 없습니다. 그런 것은 내장되어 있지 않습니다.

가장 존경받는 Stephen Toub가 만든 AsyncSemaphore 클래스를 살펴보십시오 . 원하는 것은 세마포어라고하며 비동기 버전이 필요합니다.


12
"안타깝게도 .NET Framework에는 병렬 비동기 작업을 조정하기위한 가장 중요한 결합자가 없습니다. 이러한 기능이 내장되어 있지 않습니다." .NET 4.5 베타에서 더 이상 올바르지 않습니다. SemaphoreSlim은 이제 WaitAsync (...) 기능을 제공합니다. :)
Theo Yaung

SemaphoreSlim (새로운 비동기 메서드 포함)이 AsyncSemphore보다 선호되어야합니까, 아니면 Toub의 구현이 여전히 약간의 이점이 있습니까?
Todd Menier 2013

제 생각에 내장형은 잘 테스트되고 잘 디자인 될 가능성이 높기 때문에 선호되어야합니다.
usr

4
Stephen은 자신의 블로그 게시물에 .NET 4.5 용 SemaphoreSlim을 사용하는 것이 일반적으로 갈 길임을 확인하는 질문에 대한 답변을 추가했습니다.
jdasilva

7

많은 함정이 있으며 오류의 경우 세마포어를 직접 사용하는 것이 까다로울 수 있으므로 바퀴를 다시 발명하는 대신 AsyncEnumerator NuGet 패키지 를 사용하는 것이 좋습니다 .

// 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

Theo Yaung 예제는 좋지만 대기 작업 목록이없는 변형이 있습니다.

 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
이 방법을 사용 ProccessUrl하는 데는 한 가지 위험이 있습니다.에서 발생하는 모든 예외 또는 하위 기능은 실제로 무시됩니다. 태스크로 캡처되지만 원래 호출자에게 다시 여과되지는 않습니다 Check(...). 난 아직도 같은 작업과 콤비 기능을 사용하는 이유 개인적으로, 그건 WhenAllWhenAny더 나은 오류 전파를 얻을 -. :)
Theo Yaung

3

SemaphoreSlim은 여기에서 매우 유용 할 수 있습니다. 내가 만든 확장 방법은 다음과 같습니다.

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

샘플 사용법 :

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

0

오래된 질문, 새로운 답변. @vitidev에는 내가 검토 한 프로젝트에서 거의 그대로 재사용 된 코드 블록이 있습니다. 몇몇 동료들과 논의한 후 "내장 된 TPL 방법을 사용하지 않는 이유는 무엇입니까?" ActionBlock이 승자처럼 보입니다. https://msdn.microsoft.com/en-us/library/hh194773(v=vs.110).aspx . 아마도 기존 코드를 변경하지 않을 것이지만 확실히이 너겟을 채택하고 조절 된 병렬 처리에 대해 Mr. Softy의 모범 사례를 재사용 할 것입니다.


0

다음은 LINQ의 게으른 특성을 활용하는 솔루션입니다. 허용되는 답변 과 기능적으로 동일 하지만) 대신 작업자 작업을 사용 SemaphoreSlim하여 전체 작업의 메모리 공간을 줄입니다. 처음에는 스로틀 링없이 작동하도록합니다. 첫 번째 단계는 URL을 작업 목록으로 변환하는 것입니다.

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

두 번째 단계는 await다음 Task.WhenAll방법을 사용하여 모든 작업을 동시에 수행하는 것입니다.

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

산출:

URL : https://stackoverflow.com , 105.574 자
URL : https://superuser.com , 126.953 자
URL : https://serverfault.com , 125.963 자
URL : https://meta.stackexchange.com , 185.276 자
...

Microsoft의 구현Task.WhenAll제공된 열거 형을 배열에 즉시 구체화하여 모든 작업이 한 번에 시작되도록합니다. 동시 비동기 작업의 수를 제한하고 싶기 때문에 원하지 않습니다. 따라서 우리 WhenAll는 열거 형을 부드럽고 천천히 열거 할 대안을 구현해야합니다 . 많은 작업자 작업 (원하는 동시성 수준과 동일)을 생성하고 각 작업자 작업은 잠금을 사용하여 한 번에 하나의 열거 가능한 작업을 열거하여 각 URL 작업이 처리되도록합니다. 단 하나의 작업자 작업으로. 그런 다음 await모든 작업자 작업을 완료하고 마지막으로 결과를 반환합니다. 구현은 다음과 같습니다.

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

... 원하는 제한을 달성하기 위해 초기 코드에서 변경해야하는 사항은 다음과 같습니다.

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

예외 처리와 관련하여 차이가 있습니다. 네이티브 Task.WhenAll는 모든 작업이 완료 될 때까지 기다렸다가 모든 예외를 집계합니다. 위의 구현은 첫 번째 오류가 발생한 작업이 완료된 후 즉시 종료됩니다.


를 반환하는 AC # 8 구현은 여기IAsyncEnumerable<T> 에서 찾을 수 있습니다 .
Theodor Zoulias

-1

1000 개의 작업이 매우 빠르게 대기열에 추가 될 수 있지만 Parallel Tasks 라이브러리는 컴퓨터의 CPU 코어 양과 동일한 동시 작업 만 처리 할 수 ​​있습니다. 즉, 4 코어 시스템이있는 경우 주어진 시간에 4 개의 작업 만 실행됩니다 (MaxDegreeOfParallelism을 낮추지 않는 한).


8
네,하지만 비동기 I / O 작업과는 관련이 없습니다. 위의 코드는 단일 스레드에서 실행되는 경우에도 1000 개 이상의 동시 다운로드를 발생시킵니다.
Grief Coder

await거기 에서 키워드를 보지 못했습니다 . 제거하면 문제가 해결됩니다. 맞습니까?
scottm

2
라이브러리는 확실히 Running코어의 양보다 동시에 실행중인 ( 상태 와 함께) 더 많은 작업을 처리 할 수 ​​있습니다 . 특히 I / O 바운드 태스크의 경우에 해당합니다.
svick

@svick : 네. 최대 동시 TPL 작업 (스레드 아님)을 효율적으로 제어하는 ​​방법을 알고 있습니까?
Grief Coder

-1

CPU 바운드 작업의 속도를 높이려면 병렬 계산을 사용해야합니다. 여기서는 I / O 바운드 작업에 대해 설명합니다. 다중 코어 CPU에서 바쁜 단일 코어를 압도하지 않는 한 구현은 순전히 비동기 이어야합니다 .

편집 나는 여기에 "비동기 세마포어"를 사용하는 usr의 제안을 좋아합니다.


좋은 지적! 여기의 각 작업에는 비동기 및 동기화 코드가 포함됩니다 (페이지가 비동기 적으로 다운로드 된 다음 동기화 방식으로 처리됨). CPU에 코드의 동기화 부분을 배포하고 동시에 동시 비동기 I / O 작업의 양을 제한하려고합니다.
Grief Coder

왜? 1000 개 이상의 http 요청을 동시에 실행하는 것은 사용자의 네트워크 용량에 적합한 작업이 아닐 수 있기 때문입니다.
지출 자

병렬 확장은 순수한 비동기 솔루션을 수동으로 구현하지 않고도 I / O 작업을 다중화하는 방법으로 사용할 수도 있습니다. 내가 동의하는 것은 엉성한 것으로 간주 될 수 있지만 동시 작업 수에 대한 엄격한 제한을 유지하는 한 스레드 풀에 너무 많은 부담을주지 않을 것입니다.
Sean U

3
이 답변이 답을 제공한다고 생각하지 않습니다. 여기서는 순전히 비동기만으로는 충분하지 않습니다. 우리는 실제로 비 차단 방식으로 물리적 IO를 제한하고 싶습니다.
usr

1
흠 .. 저도 동의하지 않습니다. 대규모 프로젝트를 작업 할 때 너무 많은 개발자가이 관점을 취하면 각 개발자의 단독 기여만으로는 문제를 해결하기에 충분하지 않더라도 굶주릴 것입니다. ThreadPool 이 하나 뿐이라는 점을 감안할 때 반 정중하게 취급하더라도 다른 사람들이 똑같이하고 있다면 문제가 발생할 수 있습니다. 따라서 저는 항상 ThreadPool에서 긴 작업을 실행하지 않는 것이 좋습니다.
지출

-1

MaxDegreeOfParallelism에서 지정할 수있는 옵션 인을 사용하십시오 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
나는 이것이 효과가 있다고 생각하지 않는다. GetStringAsync(url)와 함께 호출됩니다 await. 유형을 검사 하면 결과가 아니라 var htmla 입니다. Task<string>string
Neal Ehardt 2015

2
@NealEhardt가 정확합니다. 동기 코드 Parallel.ForEach(...)블록을 병렬 로 실행하기위한 것입니다 (예 : 다른 스레드에서).
Theo Yaung

-1

기본적으로 적중하려는 각 URL에 대해 작업 또는 작업을 만들고 목록에 넣은 다음 해당 목록을 처리하여 병렬로 처리 할 수있는 수를 제한하려고합니다.

내 블로그 게시물 은 Tasks와 Actions를 사용하여이 작업을 수행하는 방법을 보여주고, 다운로드하여 실행할 수있는 샘플 프로젝트를 제공합니다.

액션

Actions를 사용하는 경우 내장 된 .Net Parallel.Invoke 함수를 사용할 수 있습니다. 여기서는 최대 20 개의 스레드를 병렬로 실행하도록 제한합니다.

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

작업 포함

Tasks에는 기본 제공 기능이 없습니다. 그러나 내 블로그에서 제공하는 것을 사용할 수 있습니다.

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

그런 다음 작업 목록을 만들고 한 번에 최대 20 개를 동시에 실행하도록 함수를 호출하면 다음과 같이 할 수 있습니다.

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

나는 당신이 SemaphoreSlim에 대해 initialCount를 지정하고 있다고 생각하며 SemaphoreSlim의 생성자에서 두 번째 매개 변수 즉 maxCount를 지정해야합니다.
Jay Shah

각 작업의 각 응답을 목록으로 처리하고 싶습니다. 반환 결과 또는 응답을 받으려면 어떻게해야합니까?
venkat

-1

이것은 전역 변수를 변경하므로 좋은 습관이 아닙니다. 또한 비동기에 대한 일반적인 솔루션이 아닙니다. 하지만 HttpClient의 모든 인스턴스가 그게 전부라면 쉽습니다. 간단히 시도 할 수 있습니다.

System.Net.ServicePointManager.DefaultConnectionLimit = 20;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.