Parallel.ForEach เทียบกับ Task.Fartory.StartNew


267

ความแตกต่างระหว่างตัวอย่างโค้ดด้านล่างคืออะไร ทั้งสองจะไม่ใช้เธรดพูลหรือไม่

เช่นถ้าฉันต้องการเรียกใช้ฟังก์ชันสำหรับแต่ละรายการในคอลเลกชัน

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

คำตอบ:


302

แรกคือตัวเลือกที่ดีกว่ามาก

Parallel.ForEach ภายในใช้Partitioner<T>เพื่อแจกจ่ายคอลเลกชันของคุณไปยังรายการงาน มันจะไม่ทำงานหนึ่งรายการต่อหนึ่งรายการ แต่ควรทำแบบนี้เพื่อลดค่าใช้จ่ายที่เกี่ยวข้อง

ตัวเลือกที่สองจะกำหนดตารางเวลาหนึ่งTaskรายการในคอลเลกชันของคุณ ในขณะที่ผลลัพธ์จะเป็น (เกือบ) เหมือนกันสิ่งนี้จะแนะนำค่าใช้จ่ายมากกว่าที่จำเป็นโดยเฉพาะอย่างยิ่งสำหรับคอลเลกชันขนาดใหญ่และทำให้เวลาทำงานโดยรวมจะช้าลง

FYI - The Partitioner ที่ใช้สามารถควบคุมได้โดยใช้overloadsที่เหมาะสมเพื่อ Parallel.ForEachหากต้องการ สำหรับรายละเอียดโปรดดูCustom Partitionersบน MSDN

ความแตกต่างหลัก ณ รันไทม์คือวินาทีจะทำหน้าที่แบบอะซิงโครนัส สามารถทำซ้ำได้โดยใช้ Parallel.ForEach โดยทำ:

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

ด้วยการทำเช่นนี้คุณยังคงได้รับประโยชน์จากพาร์ทิชัน แต่อย่าปิดกั้นจนกว่าการดำเนินการจะเสร็จสมบูรณ์


8
IIRC การแบ่งพาร์ติชันเริ่มต้นโดย Parallel นอกจากนี้ ForEach ยังคำนึงถึงจำนวนเธรดฮาร์ดแวร์ที่มีอยู่ซึ่งจะช่วยให้คุณไม่ต้องทำงานจำนวนที่เหมาะสมในการเริ่มต้น ลองอ่านบทความรูปแบบการเขียนโปรแกรมแบบขนานของ Microsoft ; มันมีคำอธิบายที่ยอดเยี่ยมเกี่ยวกับสิ่งนี้ทั้งหมดในนั้น
Mal Ross

2
@Mal: เรียงจาก ... นั่นไม่ใช่พาร์ทิชัน แต่จริงๆแล้วเป็นงานของ TaskScheduler โดยค่าเริ่มต้น TaskScheduler ใช้ ThreadPool ใหม่ซึ่งจัดการได้เป็นอย่างดีในตอนนี้
Reed Copsey

ขอบคุณ ฉันรู้ว่าฉันควรจะออกจาก "ฉันไม่มีความเชี่ยวชาญ แต่ ... " ข้อแม้ :)
Mal Ross

@ReedCopsey: วิธีแนบภารกิจที่เริ่มต้นผ่าน Parallel.ForEach กับภารกิจ wrapper ได้อย่างไร ดังนั้นเมื่อคุณเรียกว่า. Wait () ในงาน wrapper มันจะหยุดทำงานจนกว่างานที่ทำงานแบบขนานจะเสร็จสมบูรณ์
Konstantin Tarkus

1
@ ทาร์คุสหากคุณทำหลายคำขอคุณจะดีกว่าเพียงใช้ HttpClient.GetString ในแต่ละไอเท็มงาน (ใน Parallel loop) ไม่มีเหตุผลที่จะนำภายในตัวเลือก async ของห่วงพร้อมกันแล้วมักจะ ...
Reed Copsey

89

ฉันทำการทดลองใช้วิธี "1,000,000,000 (หนึ่งพันล้านครั้ง)" ครั้งเล็ก ๆ ด้วย "Parallel.For" และอีกหนึ่งเป็นวัตถุ "Task"

ฉันวัดเวลาโปรเซสเซอร์และพบว่า Parallel มีประสิทธิภาพมากขึ้น Parallel.For แบ่งงานของคุณเป็นรายการงานขนาดเล็กและดำเนินการพวกเขาในทุกแกนอย่างเท่าเทียมกันในวิธีที่ดีที่สุด ในขณะที่สร้างวัตถุงานจำนวนมาก (FYI TPL จะใช้เธรดร่วมกันภายใน) จะย้ายทุกการดำเนินการในแต่ละงานสร้างความเครียดมากขึ้นในกล่องซึ่งเห็นได้จากการทดลองด้านล่าง

ฉันได้สร้างวิดีโอขนาดเล็กซึ่งอธิบาย TPL พื้นฐานและยังแสดงให้เห็นว่า Parallel.For ใช้ประโยชน์จากแกนของคุณอย่างมีประสิทธิภาพมากขึ้นhttp://www.youtube.com/watch?v=No7QqSc5cl8เมื่อเปรียบเทียบกับงานและเธรดปกติ

การทดลอง 1

Parallel.For(0, 1000000000, x => Method1());

การทดลอง 2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

การเปรียบเทียบเวลาตัวประมวลผล


มันจะมีประสิทธิภาพมากกว่าและสาเหตุที่การสร้างเธรดนั้นมีค่าใช้จ่ายสูงการทดลองที่ 2 นั้นเป็นวิธีปฏิบัติที่แย่มาก
ทิม

@ Georgi- โปรดดูแลเกี่ยวกับการพูดคุยเพิ่มเติมเกี่ยวกับสิ่งที่ไม่ดี
Shivprasad Koirala

3
ฉันขอโทษผิดฉันควรจะชี้แจง ฉันหมายถึงการสร้างงานในลูปถึง 1000000000 ค่าใช้จ่ายเป็นไปไม่ได้ ไม่ต้องพูดถึงว่า Parallel ไม่สามารถสร้างงานได้มากกว่า 63 งานในแต่ละครั้งซึ่งทำให้มีประสิทธิภาพสูงสุดในกรณีนี้
Georgi-it

สิ่งนี้เป็นจริงสำหรับงาน 1000000000 อย่างไรก็ตามเมื่อฉันประมวลผลภาพ (ซ้ำ ๆ , การซูมเศษส่วน) และทำแบบขนานสำหรับบรรทัดจำนวนมากของคอร์จะไม่ได้ใช้งานในขณะที่รอเธรดสุดท้ายจะเสร็จสิ้น เพื่อให้เร็วขึ้นฉันแบ่งข้อมูลออกเป็น 64 แพ็คเกจงานและสร้างงานให้ (จากนั้น Task.WaitAll เพื่อรอให้เสร็จ) แนวคิดคือให้เธรดที่ไม่ทำงานหยิบแพ็กเกจงานเพื่อช่วยทำงานให้เสร็จแทนที่จะรอ 1-2 เธรดเพื่อทำให้เสร็จ (Parallel.For) อันที่มอบหมาย
Tedd Hansen

1
สิ่งที่ไม่Mehthod1()ทำในตัวอย่างนี้?
Zapnologica

17

Parallel.ForEach จะปรับให้เหมาะสม (อาจไม่ได้เริ่มเธรดใหม่) และบล็อกจนกว่าการวนซ้ำจะเสร็จสิ้นและ Task.Factory จะสร้างอินสแตนซ์งานใหม่สำหรับแต่ละรายการอย่างชัดเจนและกลับมาก่อนที่จะเสร็จสิ้น (งานอะซิงโครนัส) Parallel.Foreach มีประสิทธิภาพมากขึ้น


11

ในมุมมองของฉันสถานการณ์ที่สมจริงที่สุดคือเมื่องานมีการดำเนินการอย่างหนักเพื่อให้เสร็จสมบูรณ์ วิธีการของ Shivprasad มุ่งเน้นไปที่การสร้างวัตถุ / การจัดสรรหน่วยความจำมากกว่าในการคำนวณเอง ฉันทำวิจัยเรียกวิธีต่อไปนี้:

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

การดำเนินการของวิธีนี้ใช้เวลาประมาณ 0.5 วินาที

ฉันเรียกมันว่า 200 ครั้งโดยใช้ Parallel:

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

จากนั้นฉันก็เรียกมันว่า 200 ครั้งโดยใช้วิธีที่ล้าสมัย:

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

กรณีแรกเสร็จสมบูรณ์ใน 26656ms วินาทีใน 24478ms ฉันทำซ้ำหลายครั้ง ทุกครั้งที่วิธีที่สองนั้นรวดเร็วกว่าเล็กน้อย


การใช้ Parallel.For เป็นวิธีที่ล้าสมัย ขอแนะนำให้ใช้งานสำหรับหน่วยงานที่ไม่เหมือนกัน Microsoft MVP และนักออกแบบของ TPL ยังกล่าวถึงว่าการใช้ Tasks จะใช้เธรดได้อย่างมีประสิทธิภาพมากขึ้นและไม่บล็อกเท่ากันในขณะที่รอยูนิตอื่นให้เสร็จสมบูรณ์
Suncat2000
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.