จะ จำกัด ปริมาณการดำเนินการ async 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);
});

นี่คือปัญหาเริ่มต้นคำขอเว็บพร้อมกันมากกว่า 1,000 รายการ มีวิธีง่ายๆในการ จำกัด จำนวนคำขอ async 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คุณควรกำจัดทิ้งโดยเฉพาะอย่างยิ่งเมื่อคุณจะใช้มากกว่า 1,000 รายการ HttpClientสามารถใช้เป็นซิงเกิลตันสำหรับคำขอหลายรายการ
Shimmy Weitzhandler

คำตอบ:


161

คุณสามารถทำได้ใน async เวอร์ชันล่าสุดสำหรับ. NET โดยใช้. NET 4.5 Beta โพสต์ก่อนหน้าจาก 'usr' ชี้ไปที่บทความดีๆที่เขียนโดย Stephen Toub แต่ข่าวที่ประกาศน้อยกว่าคือสัญญาณ async ทำให้มันกลายเป็นรุ่นเบต้าของ. NET 4.5

หากคุณดูSemaphoreSlimคลาสที่รักของเรา(ซึ่งคุณควรใช้เนื่องจากมีประสิทธิภาพมากกว่าเดิมSemaphore) ตอนนี้มีWaitAsync(...)ชุดของการโอเวอร์โหลดพร้อมข้อโต้แย้งที่คาดไว้ทั้งหมด - ช่วงเวลาหมดเวลาโทเค็นการยกเลิกเพื่อนที่ตั้งเวลาตามปกติทั้งหมดของคุณ: )

สตีเฟ่นยังเขียนบล็อกโพสต์ล่าสุดเพิ่มเติมเกี่ยวกับ .NET ใหม่ 4.5 สารพัดที่ออกมาพร้อมกับเบต้าเห็นมีอะไรใหม่สำหรับความเท่าเทียมใน .NET 4.5 Beta

สุดท้ายต่อไปนี้เป็นโค้ดตัวอย่างเกี่ยวกับวิธีใช้ SemaphoreSlim สำหรับการควบคุมปริมาณวิธี 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)
}

สุดท้าย แต่น่าจะเป็นการกล่าวถึงอย่างคุ้มค่าคือโซลูชันที่ใช้การตั้งเวลาตาม TPL คุณสามารถสร้างงานผูกมัดผู้รับมอบสิทธิ์บน TPL ที่ยังไม่เริ่มทำงานและอนุญาตให้ตัวกำหนดตารางเวลางานแบบกำหนดเอง จำกัด การทำงานพร้อมกัน อันที่จริงมีตัวอย่าง MSDN อยู่ที่นี่:

โปรดดูTaskSchedulerด้วย


3
ไม่ใช่เส้นขนานก่อนที่จะมีความเท่าเทียมกันในระดับ จำกัด วิธีที่ดีกว่า? msdn.microsoft.com/en-us/library/…
GreyCloud

2
ทำไมคุณไม่กำจัดคุณHttpClient
Shimmy Weitzhandler

4
@GreyCloud: Parallel.ForEachทำงานร่วมกับรหัสซิงโครนัส สิ่งนี้ช่วยให้คุณสามารถเรียกรหัสอะซิงโครนัสได้
Josh Noe

2
@TheMonarch คุณกำลังผิด นอกจากนี้ก็มักจะเป็นนิสัยที่ดีในการห่อทั้งหมดIDisposableในusingหรือtry-finallyงบและมั่นใจกำจัดของพวกเขา
Shimmy Weitzhandler

29
เนื่องจากคำตอบนี้ได้รับความนิยมเพียงใดจึงควรชี้ให้เห็นว่า HttpClient สามารถและควรเป็นอินสแตนซ์ทั่วไปเดียวแทนที่จะเป็นอินสแตนซ์ต่อคำขอ
Rupert Rawnsley

15

หากคุณมี IEnumerable (เช่นสตริงของ URL s) และคุณต้องการดำเนินการผูก I / O กับแต่ละสิ่งเหล่านี้ (เช่นสร้างคำขอ async http) พร้อมกันและคุณยังต้องการกำหนดจำนวนสูงสุดของการทำงานพร้อมกัน คำขอ I / O แบบเรียลไทม์นี่คือวิธีที่คุณสามารถทำได้ วิธีนี้คุณไม่ใช้เธรดพูลและคณะวิธีนี้ใช้เซมาโฟเรสลิมเพื่อควบคุมคำขอ I / O พร้อมกันสูงสุดที่คล้ายกับรูปแบบหน้าต่างบานเลื่อนที่คำขอหนึ่งเสร็จสมบูรณ์ออกจากเซมาฟอร์และคำขอถัดไปเข้ามา

การใช้งาน: รอ 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 ซึ่งในกรณีนี้เราจำเป็นต้องกำจัดหรือห่อไว้ภายในบล็อกที่ใช้
Dogu Arslan

1
เพียงแค่คิดถึงแนวทางปฏิบัติที่ดีที่สุดและบทเรียนที่เราสอนคนอื่น ๆ usingจะดี
AgentFire

ตัวอย่างนี้ฉันสามารถทำตามได้ แต่พยายามหาวิธีที่ดีที่สุดในการทำสิ่งนี้โดยทั่วไปมีตัวควบคุมปริมาณ แต่ Func ของฉันจะส่งคืนรายการซึ่งในที่สุดฉันก็ต้องการในรายการสุดท้ายของทั้งหมดที่เสร็จสิ้นเมื่อเสร็จสิ้น ... ซึ่งอาจ ต้องการล็อคในรายการคุณมีข้อเสนอแนะ
Seabizkit

คุณสามารถอัปเดตวิธีการเล็กน้อยเพื่อให้ส่งคืนรายการงานจริงและคุณรอ 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 ไม่มีตัวรวมที่สำคัญที่สุดสำหรับการจัดเตรียมงาน async แบบขนาน ไม่มีสิ่งนี้ในตัว

ดูคลาสAsyncSemaphore ที่สร้างโดย Stephen Toub ที่น่านับถือที่สุด สิ่งที่คุณต้องการเรียกว่าเซมาฟอร์และคุณต้องมีเวอร์ชัน async


12
โปรดทราบว่า "น่าเสียดายที่. NET Framework ไม่มีตัวรวมที่สำคัญที่สุดสำหรับการจัดเตรียมงาน async แบบขนานไม่มีสิ่งนี้ในตัว" ไม่ถูกต้องอีกต่อไปสำหรับ. NET 4.5 Beta SemaphoreSlim เสนอฟังก์ชัน WaitAsync (... ) แล้ว :)
Theo Yaung

SemaphoreSlim (ด้วยวิธีการ async ใหม่) ควรเป็นที่ต้องการมากกว่า AsyncSemphore หรือการใช้งานของ Toub ยังคงมีข้อได้เปรียบอยู่บ้าง?
Todd Menier

ในความคิดของฉันควรเลือกประเภทบิวท์อินเพราะน่าจะผ่านการทดสอบและออกแบบมาอย่างดี
usr

4
Stephen เพิ่มความคิดเห็นเพื่อตอบคำถามในบล็อกโพสต์ของเขาเพื่อยืนยันว่าการใช้ SemaphoreSlim สำหรับ. NET 4.5 โดยทั่วไปจะเป็นหนทางที่จะไป
jdasilva

7

มีข้อผิดพลาดมากมายและการใช้เซมาฟอร์โดยตรงอาจเป็นเรื่องยุ่งยากในกรณีที่มีข้อผิดพลาดดังนั้นฉันขอแนะนำให้ใช้AsyncEnumerator NuGet Packageแทนการประดิษฐ์วงล้อใหม่:

// 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(...)แต่ไม่กระจายกลับไปที่โทรเดิมของ โดยส่วนตัวแล้วนั่นเป็นเหตุผลที่ฉันยังคงใช้ Tasks และฟังก์ชัน combinator เช่นWhenAllและWhenAny- เพื่อให้การเผยแพร่ข้อผิดพลาดดีขึ้น :)
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.


0

นี่คือวิธีแก้ปัญหาที่ใช้ประโยชน์จากลักษณะขี้เกียจของ LINQ ฟังก์ชันนี้เทียบเท่ากับคำตอบที่ยอมรับ ) แต่ใช้งานของผู้ปฏิบัติงานแทน a 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 chars
Url: https://superuser.com , 126.953 chars
Url: https://serverfault.com , 125.963 chars
Url: https://meta.stackexchange.com , 185.276 chars
...

การใช้งานไมโครซอฟท์ของTask.WhenAllmaterializes ทันทีที่จัดมานับไปยังอาร์เรย์ทำให้งานทั้งหมดที่จะเริ่มต้นในครั้งเดียว เราไม่ต้องการเช่นนั้นเพราะเราต้องการ จำกัด จำนวนการดำเนินการแบบอะซิงโครนัสพร้อมกัน ดังนั้นเราต้องใช้ทางเลือกอื่นWhenAllที่จะแจกแจงการนับของเราอย่างนุ่มนวลและช้าๆ เราจะทำเช่นนั้นโดยการสร้างงานของผู้ปฏิบัติงานจำนวนหนึ่ง (เท่ากับระดับของการทำงานพร้อมกันที่ต้องการ) และแต่ละงานของผู้ปฏิบัติงานจะแจกแจงงานทีละรายการโดยใช้การล็อกเพื่อให้แน่ใจว่าแต่ละงานจะได้รับการประมวลผล โดยผู้ปฏิบัติงานเพียงคนเดียว จากนั้นเรา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

แม้ว่างาน 1,000 งานอาจถูกจัดคิวอย่างรวดเร็ว แต่ไลบรารี Parallel Tasks สามารถจัดการงานพร้อมกันได้เท่ากับจำนวนแกน CPU ในเครื่องเท่านั้น นั่นหมายความว่าหากคุณมีเครื่องสี่คอร์จะมีเพียง 4 งานเท่านั้นที่จะดำเนินการในเวลาที่กำหนด (เว้นแต่คุณจะลด MaxDegreeOfParallelism)


8
ใช่ แต่นั่นไม่เกี่ยวข้องกับการดำเนินการ async I / O โค้ดด้านบนจะเริ่มการดาวน์โหลดพร้อมกันมากกว่า 1,000 รายการแม้ว่าจะทำงานบนเธรดเดียวก็ตาม
Grief Coder

ไม่เห็นawaitคีย์เวิร์ดในนั้น การถอดนั้นควรแก้ปัญหาได้ถูกต้องหรือไม่?
scottm

2
ไลบรารีสามารถรองรับงานที่รัน (พร้อมRunningสถานะ) พร้อมกันได้มากกว่าจำนวนคอร์ โดยเฉพาะอย่างยิ่งในกรณีที่มีงานผูก I / O
svick

@svick: ครับ คุณรู้วิธีควบคุมงาน TPL พร้อมกันสูงสุดอย่างมีประสิทธิภาพ (ไม่ใช่เธรด) หรือไม่?
Grief Coder

-1

ควรใช้การคำนวณแบบขนานเพื่อเร่งการทำงานที่เชื่อมต่อกับ CPU ที่นี่เรากำลังพูดถึงการดำเนินการที่ถูกผูกไว้ของ I / O การใช้งานของคุณควรเป็นแบบasync อย่างแท้จริงเว้นแต่ว่าคุณจะมีแกนเดี่ยวที่ยุ่งอยู่ใน CPU แบบ multi-core ของคุณ

แก้ไข ฉันชอบคำแนะนำของ usr ในการใช้ "async semaphore" ที่นี่


จุดดี! แม้ว่าแต่ละงานจะมีรหัส async และ sync (หน้าที่ดาวน์โหลดแบบอะซิงโครนัสแล้วประมวลผลในลักษณะการซิงค์) ฉันกำลังพยายามแจกจ่ายส่วนการซิงค์ของรหัสผ่านซีพียูและในเวลาเดียวกันก็ จำกัด จำนวนการดำเนินการ async I / O พร้อมกัน
Grief Coder

ทำไม? เนื่องจากการเรียกใช้คำขอ http มากกว่า 1,000 รายการพร้อมกันอาจไม่เหมาะกับความจุเครือข่ายของผู้ใช้
ผู้ใช้จ่ายใน

นอกจากนี้ยังสามารถใช้ส่วนขยายแบบขนานเป็นวิธีในการดำเนินการมัลติเพล็กซ์ I / O โดยไม่ต้องใช้โซลูชัน Pure async ด้วยตนเอง ซึ่งฉันเห็นด้วยว่าอาจถือได้ว่าเป็นเรื่องที่เลอะเทอะ แต่ตราบใดที่คุณ จำกัด จำนวนการดำเนินการพร้อมกันไว้แน่นก็คงไม่ทำให้เธรดพูลเครียดเกินไป
Sean U

3
ฉันไม่คิดว่าคำตอบนี้จะให้คำตอบ การ async อย่างหมดจดนั้นไม่เพียงพอที่นี่: เราต้องการเค้น 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 htmlมันเป็นไม่ได้ผลTask<string> string
Neal Ehardt

2
@NealEhardt ถูกต้อง Parallel.ForEach(...)มีไว้สำหรับการรันบล็อกของรหัสซิงโครนัสแบบขนาน (เช่นบนเธรดที่แตกต่างกัน)
Theo Yaung

-1

โดยพื้นฐานแล้วคุณจะต้องการสร้าง Action หรือ Task สำหรับแต่ละ URL ที่คุณต้องการตีใส่ไว้ใน List จากนั้นประมวลผลรายการนั้นโดย จำกัด จำนวนที่สามารถประมวลผลควบคู่กันได้

โพสต์บล็อกของฉันแสดงวิธีดำเนินการทั้งกับ 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

ด้วย 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);

ฉันคิดว่าคุณแค่ระบุ initialCount สำหรับ SemaphoreSlim และคุณต้องระบุพารามิเตอร์ที่ 2 เช่น maxCount ในตัวสร้างของ SemaphoreSlim
Jay Shah

ฉันต้องการให้คำตอบจากแต่ละงานประมวลผลเป็นรายการ ฉันจะได้รับผลตอบแทนหรือการตอบกลับได้อย่างไร
venkat

-1

นี่ไม่ใช่แนวทางปฏิบัติที่ดีเนื่องจากมีการเปลี่ยนแปลงตัวแปรส่วนกลาง นอกจากนี้ยังไม่ใช่วิธีแก้ปัญหาทั่วไปสำหรับ async แต่มันง่ายสำหรับทุกอินสแตนซ์ของ HttpClient ถ้านั่นคือทั้งหมดที่คุณต้องการ คุณสามารถลอง:

System.Net.ServicePointManager.DefaultConnectionLimit = 20;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.