TPL Dataflow เทียบกับ Semaphore ธรรมดา

ฉันมีข้อกำหนดในการสร้างกระบวนการที่ปรับขนาดได้ กระบวนการนี้มีการดำเนินการ I/O เป็นหลัก โดยมีการดำเนินการ CPU เล็กน้อยบางส่วน (ส่วนใหญ่เป็นสตริงการดีซีเรียลไลซ์) กระบวนการค้นหาฐานข้อมูลเพื่อดูรายการ URL จากนั้นดึงข้อมูลจาก URL เหล่านี้ ดีซีเรียลไลซ์ข้อมูลที่ดาวน์โหลดไปยังอ็อบเจ็กต์ จากนั้นคงข้อมูลบางส่วนไว้ใน CRM Dynamics และไปยังฐานข้อมูลอื่นด้วย หลังจากนั้นฉันต้องอัปเดตฐานข้อมูลแรกที่มีการประมวลผล URL ส่วนหนึ่งของข้อกำหนดคือการทำให้สามารถกำหนดค่าระดับความขนานได้

ในตอนแรก ฉันคิดว่าจะใช้มันตามลำดับงานโดยมีการรอคอยและจำกัดความเท่าเทียมโดยใช้ Semaphore ซึ่งค่อนข้างง่าย จากนั้นฉันก็อ่านโพสต์และคำตอบบางส่วนที่นี่ของ @Stephen Cleary ซึ่งแนะนำให้ใช้ TPL Dataflow และฉันคิดว่านี่อาจเป็นตัวเลือกที่ดี อย่างไรก็ตาม ฉันต้องการให้แน่ใจว่าฉันกำลัง "ทำให้โค้ดซับซ้อน" โดยใช้ Dataflow เพื่อสาเหตุที่สมควร ฉันยังได้รับคำแนะนำให้ใช้ วิธีการขยาย ForEachAsync ซึ่งใช้งานง่ายเช่นกัน แต่ฉันไม่แน่ใจว่ามันจะไม่ทำให้เกิดโอเวอร์เฮดหน่วยความจำหรือไม่เนื่องจากวิธีที่แบ่งพาร์ติชันคอลเลกชัน

TPL Dataflow เป็นตัวเลือกที่ดีสำหรับสถานการณ์เช่นนี้หรือไม่ จะดีกว่าวิธี Semaphore หรือ ForEachAsync อย่างไร - ฉันจะได้ประโยชน์อะไรบ้างหากใช้งานผ่าน TPL DataFlow เหนือตัวเลือกอื่นๆ (Semaphore/ForEachASync)


person BornToCode    schedule 31.07.2018    source แหล่งที่มา
comment
Tpl Dataflow ดีกว่าสำหรับงาน cpu สำหรับการเรียก async I/o ฉันจะใช้ Task.WhenAll กับงานต่างๆ   -  person Peter Bons    schedule 31.07.2018
comment
@PeterBons - สถานการณ์ของฉันมีการเรียก I/O เป็นหลัก แต่ยังใช้งาน cpu เล็กน้อย (เช่นการดีซีเรียลไลซ์เนื้อหาของไฟล์) ฉันสามารถนำไปใช้กับเซมาฟอร์ได้ แต่รู้สึกว่าฉันจะมีประสิทธิภาพเพิ่มขึ้นโดยใช้ Tpl Dataflow แต่ฉันยังไม่แน่ใจว่าฉันเข้าใจประโยชน์ของ Dataflow อย่างถ่องแท้ เพื่อจะได้ตัดสินใจได้ว่าคุ้มค่าหรือไม่ เพราะอาจทำให้โค้ดของฉันซับซ้อนกว่าการใช้เซมาฟอร์เพียงอย่างเดียว   -  person BornToCode    schedule 31.07.2018
comment
ฉันสนใจที่จะขอความเห็นจากผู้เชี่ยวชาญเกี่ยวกับเรื่องนี้จริงๆ ฉันกำลังทำสิ่งเดียวกันกับคุณทุกประการ และตัดสินใจไม่ได้ระหว่างเซมาฟอร์กับ TPL Dataflow ฉันกำลังโน้มตัวไปทางการใช้ ActionBlock โดยที่ MaxDegreeOfParallelism สามารถกำหนดค่าได้ จากสิ่งที่ฉันเข้าใจ TPL จัดการเธรดพูลให้คุณอย่างมีประสิทธิภาพ แต่มี ปัญหาอื่นๆ บางประการ ฉันอยากให้มันเรียบง่าย แค่จำกัดจำนวนงานที่ทำในคราวเดียว นั่นคือสิ่งที่คุณกำลังทำอยู่หรือเปล่า?   -  person Polynomial Proton    schedule 31.07.2018
comment
โอ้ แต่ลองดู คำตอบจาก @Stephen Cleary TPL Dataflow is great, especially if you're looking to limit work in one part of a larger pipeline อย่างไรก็ตาม หากมีการเร่งความเร็วเพียงครั้งเดียว สัญญาณก็เพียงพอแล้ว   -  person Polynomial Proton    schedule 31.07.2018
comment
@TheUknown - ข่าวดี เราได้รับคำตอบจากผู้เชี่ยวชาญแล้ว :) เป้าหมายของฉันไม่เพียงแค่จำกัดจำนวนงานเท่านั้น แต่ยังต้องแน่ใจว่ากระบวนการทั้งหมดเสร็จสิ้นโดยเร็วที่สุด โดยรู้ว่าส่วนที่เขียนถึง Crm คือคอขวดหลัก ขอขอบคุณที่ให้ข้อมูลอ้างอิงความคิดเห็นของคุณกับคำตอบอื่น ๆ แต่ก็ให้ข้อมูลและเหมาะกับสถานการณ์ของฉันด้วย   -  person BornToCode    schedule 01.08.2018


คำตอบ (2)


กระบวนการนี้มีการดำเนินการ IO เป็นหลัก โดยมีการดำเนินการของ CPU เล็กน้อย (ส่วนใหญ่เป็นสตริงการดีซีเรียลไลซ์)

มันก็แค่ I/O เท่านั้น เว้นแต่ว่าสตริงเหล่านั้น ใหญ่ การดีซีเรียลไลซ์ไลซ์จะไม่คุ้มกับการขนาน งาน CPU ที่คุณกำลังทำอยู่จะหายไปพร้อมกับเสียงรบกวน

ดังนั้น คุณจะต้องมุ่งเน้นไปที่ความไม่ตรงกันที่เกิดขึ้นพร้อมกัน

  • SemaphoreSlim เป็นรูปแบบมาตรฐานสำหรับสิ่งนี้ ตามที่คุณพบ
  • TPL Dataflow ยังสามารถทำงานพร้อมกันได้ (ทั้งแบบอะซิงโครนัสและแบบขนาน)

ForEachAsync สามารถมีได้หลายรูปแบบ โปรดทราบว่าใน บล็อกโพสต์ ที่คุณอ้างถึง มีการใช้งานที่แตกต่างกัน 5 วิธี ซึ่งแต่ละวิธีใช้ได้ "[T] ต่อไปนี้เป็นความหมายที่แตกต่างกันมากมายที่เป็นไปได้สำหรับการวนซ้ำ และแต่ละความหมายจะส่งผลให้มีตัวเลือกการออกแบบและการใช้งานที่แตกต่างกัน" เพื่อวัตถุประสงค์ของคุณ (ไม่ต้องการให้ CPU ขนานกัน) คุณไม่ควรพิจารณาการใช้ Task.Run หรือการแบ่งพาร์ติชัน ในโลกที่เกิดพร้อมกันแบบอะซิงโครนัส การใช้งาน ForEachAsync ใดๆ ก็ตามจะเป็นเพียงแค่การใช้วากยสัมพันธ์ที่ซ่อนซีแมนทิกส์ที่ใช้ ซึ่งเป็นสาเหตุที่ฉันมักจะหลีกเลี่ยงมัน

สิ่งนี้จะทำให้คุณได้ SemaphoreSlim เทียบกับ ActionBlock โดยทั่วไป ฉันแนะนำให้ผู้คนเริ่มต้นด้วย SemaphoreSlim ก่อน และพิจารณาเปลี่ยนไปใช้ TPL Dataflow หากความต้องการของพวกเขาซับซ้อนมากขึ้น (ในลักษณะที่ดูเหมือนว่าพวกเขาจะได้ประโยชน์จากไปป์ไลน์ Dataflow)

เช่น "ส่วนหนึ่งของข้อกำหนดคือการกำหนดค่าระดับความขนานได้"

คุณอาจเริ่มต้นด้วยการอนุญาตระดับของการทำงานพร้อมกัน โดยที่สิ่งที่ถูกควบคุมคือการดำเนินการทั้งหมดเพียงครั้งเดียว (ดึงข้อมูลจาก url, ดีซีเรียลไลซ์ข้อมูลที่ดาวน์โหลดไปยังออบเจ็กต์, คงอยู่ในไดนามิกของ CRM และไปยังฐานข้อมูลอื่น และอัปเดตฐานข้อมูลแรก) นี่คือจุดที่ SemaphoreSlim จะเป็นทางออกที่สมบูรณ์แบบ

แต่คุณอาจตัดสินใจว่าคุณต้องการมีหลายปุ่ม เช่น ระดับของการทำงานพร้อมกันหนึ่งระดับสำหรับจำนวน URL ที่คุณกำลังดาวน์โหลด และระดับของการทำงานพร้อมกันที่แยกต่างหากสำหรับการคงอยู่ และระดับของการทำงานพร้อมกันที่แยกต่างหากสำหรับการอัปเดตฐานข้อมูลดั้งเดิม จากนั้นคุณจะต้องจำกัด "คิว" ที่อยู่ระหว่างจุดเหล่านี้: เฉพาะออบเจ็กต์ดีซีเรียลไลซ์ในหน่วยความจำจำนวนมาก ฯลฯ - เพื่อให้แน่ใจว่า URL ที่เร็วและมีฐานข้อมูลที่ช้าจะไม่ทำให้เกิดปัญหากับแอปของคุณที่ใช้งานมากเกินไป หน่วยความจำ. หากสิ่งเหล่านี้เป็นความหมายที่เป็นประโยชน์ แสดงว่าคุณเริ่มเข้าถึงปัญหาจากมุมมองของโฟลว์ข้อมูลแล้ว และนั่นคือจุดที่คุณอาจได้รับบริการที่ดีกว่าด้วยไลบรารี เช่น TPL Dataflow

person Stephen Cleary    schedule 31.07.2018
comment
ขอบคุณมากสำหรับคำตอบนี้ มันมีรายละเอียดและชัดเจน และคุณยังอ้างถึงตัวเลือกทั้งหมดที่ฉันพูดถึง รวมถึง ForEachAsync ด้วย! +100 :) - person BornToCode; 01.08.2018

จุดขายของแนวทาง Semaphore มีดังนี้:

  1. ความเรียบง่าย

และนี่คือจุดขายของแนวทาง TPL Dataflow:

  1. งานที่มีความเท่าเทียมเหนือความเท่าเทียมของข้อมูล
  2. การใช้ทรัพยากรให้เกิดประโยชน์สูงสุด (แบนด์วิธ, CPU, การเชื่อมต่อฐานข้อมูล)
  3. ระดับความขนานที่กำหนดค่าได้สำหรับการดำเนินการที่แตกต่างกันแต่ละรายการ
  4. ลดขนาดหน่วยความจำ

ลองทบทวนการใช้งาน Semaphore ต่อไปนี้เป็นตัวอย่าง:

string[] urls = FetchUrlsFromDB();
var cts = new CancellationTokenSource();
var semaphore = new SemaphoreSlim(10); // Degree of parallelism (DOP)
Task[] tasks = urls.Select(url => Task.Run(async () =>
{
    await semaphore.WaitAsync(cts.Token);
    try
    {
        string rawData = DownloadData(url);
        var data = Deserialize(rawData);
        PersistToCRM(data);
        MarkAsCompleted(url);
    }
    finally
    {
        semaphore.Release();
    }
})).ToArray();
Task.WaitAll(tasks);

การใช้งานข้างต้นทำให้มั่นใจได้ว่าจะมีการประมวลผล URL สูงสุด 10 รายการพร้อมกันในช่วงเวลาใดก็ตาม จะไม่มีการประสานงานระหว่างเวิร์กโฟลว์แบบขนานเหล่านี้ ตัวอย่างเช่น เป็นไปได้โดยสิ้นเชิงว่า ณ เวลาที่กำหนดเวิร์กโฟลว์ทั้ง 10 เวิร์กโฟลว์จะทำการดาวน์โหลดข้อมูล ในอีกขณะหนึ่งทั้ง 10 เวิร์กโฟลว์จะเป็นการดีซีเรียลไลซ์ข้อมูลดิบ และในอีกขณะหนึ่งทั้ง 10 เวิร์กโฟลว์จะคงข้อมูลไว้ที่ CRM นี่ยังห่างไกลจากอุดมคติ ตามหลักการแล้ว คุณต้องการให้เกิดปัญหาคอขวดของการดำเนินการทั้งหมด ไม่ว่าจะเป็นอะแดปเตอร์เครือข่าย CPU หรือเซิร์ฟเวอร์ฐานข้อมูล เพื่อให้ทำงานอย่างต่อเนื่องตลอดเวลา และไม่ถูกใช้งานน้อยเกินไป (หรือไม่ได้ใช้งานโดยสมบูรณ์) ในช่วงเวลาต่างๆ แบบสุ่ม

ข้อควรพิจารณาอีกประการหนึ่งก็คือ ความขนานที่เหมาะสมที่สุดสำหรับการดำเนินการที่ต่างกันแต่ละรายการ 10 DOP อาจเหมาะสมที่สุดสำหรับการสื่อสารกับเว็บ แต่ต่ำหรือสูงเกินไปสำหรับการสื่อสารกับฐานข้อมูล วิธีการเซมาฟอร์ไม่อนุญาตให้มีการปรับแต่งอย่างละเอียดในระดับนั้น ทางเลือกเดียวของคุณคือการประนีประนอมโดยการเลือกค่า DOP ที่ใดที่หนึ่งระหว่างค่าที่เหมาะสมที่สุดเหล่านี้

หากจำนวน URL มีขนาดใหญ่มาก เช่น 1,000,000 ดังนั้นวิธี Semaphore ข้างต้นจะทำให้ต้องพิจารณาการใช้หน่วยความจำอย่างจริงจังด้วย URL อาจมีขนาดโดยเฉลี่ย 50 ไบต์ ในขณะที่ Task ที่เชื่อมต่อกับ CancellationToken อาจหนักกว่า 10 เท่าหรือมากกว่านั้น แน่นอนคุณสามารถเปลี่ยนการใช้งานและใช้ SemaphoreSlim ด้วยวิธีที่ชาญฉลาดกว่าซึ่งไม่ได้สร้างงานมากมายนัก แต่สิ่งนี้ขัดกับจุดขายหลัก (และจุดเดียว) ของแนวทางนี้ นั่นคือความเรียบง่าย

ไลบรารี TPL Dataflow แก้ไขปัญหาเหล่านี้ทั้งหมด โดยเสียค่าใช้จ่ายในการเรียนรู้ (เล็กน้อย) ที่จำเป็นเพื่อให้สามารถเชื่องเครื่องมืออันทรงพลังนี้ได้

person Theodor Zoulias    schedule 11.06.2020