Parallel.ForEach vs Task.Factory.StartNew

267

Apa perbedaan antara cuplikan kode di bawah ini? Tidakkah keduanya menggunakan utas threadpool?

Misalnya jika saya ingin memanggil fungsi untuk setiap item dalam koleksi,

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

vs

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

Jawaban:

302

Yang pertama adalah pilihan yang jauh lebih baik.

Parallel.ForEach, secara internal, menggunakan a Partitioner<T>untuk mendistribusikan koleksi Anda ke item pekerjaan. Itu tidak akan melakukan satu tugas per item, tetapi batch ini untuk menurunkan overhead yang terlibat.

Opsi kedua akan menjadwalkan satu Taskper item dalam koleksi Anda. Walaupun hasilnya akan (hampir) sama, ini akan menghasilkan overhead yang jauh lebih banyak dari yang diperlukan, terutama untuk koleksi besar, dan menyebabkan runtime keseluruhan menjadi lebih lambat.

FYI - Partisi yang digunakan dapat dikontrol dengan menggunakan overload yang sesuai ke Paralel. Untuk Setiap , jika diinginkan. Untuk detailnya, lihat Pemisah Kustom di MSDN.

Perbedaan utama, pada saat runtime, adalah yang kedua akan bertindak asinkron. Ini dapat diduplikasi menggunakan Paralel. FOREach dengan melakukan:

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

Dengan melakukan ini, Anda masih mengambil keuntungan dari partisi, tetapi jangan memblokir sampai operasi selesai.

Reed Copsey
sumber
8
IIRC, partisi default yang dilakukan oleh Parallel. ForEach juga memperhitungkan jumlah utas perangkat keras yang tersedia, menyelamatkan Anda dari keharusan mencari jumlah Tugas yang optimal untuk memulai. Lihat artikel Pola Pemrograman Paralel Microsoft ; ada penjelasan bagus tentang semua hal ini di dalamnya.
Mal Ross
2
@ Mal: ​​Semacam ... Itu sebenarnya bukan Pemisah, melainkan pekerjaan dari TaskScheduler. TaskScheduler, secara default, menggunakan ThreadPool baru, yang menangani ini dengan sangat baik sekarang.
Reed Copsey
Terima kasih. Aku tahu aku seharusnya pergi dalam peringatan "Aku bukan ahli, tapi ...". :)
Mal Ross
@ReedCopsey: Bagaimana cara melampirkan tugas yang dimulai melalui Parallel. FOREach ke tugas pembungkus? Sehingga saat Anda menelepon. Tunggu () pada tugas pembungkus, hang sampai tugas yang berjalan secara paralel selesai?
Konstantin Tarkus
1
@Tarkus Jika Anda membuat beberapa permintaan, Anda lebih baik hanya menggunakan HttpClient.GetString di setiap item pekerjaan (dalam loop Paralel Anda). Tidak ada alasan untuk menempatkan opsi async di dalam loop yang sudah bersamaan, biasanya ...
Reed Copsey
89

Saya melakukan percobaan kecil menjalankan metode "1.000.000.000 (satu miliar)" kali dengan "Paralel. Untuk" dan satu dengan objek "Tugas".

Saya mengukur waktu prosesor dan menemukan Paralel lebih efisien. Paralel. Untuk membagi tugas Anda menjadi beberapa item kerja kecil dan menjalankannya pada semua core secara paralel dengan cara yang optimal. Saat membuat banyak objek tugas (FYI TPL akan menggunakan thread pooling secara internal) akan memindahkan setiap eksekusi pada setiap tugas yang menciptakan lebih banyak tekanan di dalam kotak yang terbukti dari percobaan di bawah ini.

Saya juga telah membuat video kecil yang menjelaskan TPL dasar dan juga mendemonstrasikan bagaimana Parallel.For menggunakan inti Anda dengan lebih efisien http://www.youtube.com/watch?v=No7QqSc5cl8 dibandingkan dengan tugas dan utas normal.

Eksperimen 1

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

Eksperimen 2

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

Perbandingan waktu prosesor

Shivprasad Koirala
sumber
Akan lebih efisien dan alasan di balik membuat thread itu mahal. Percobaan 2 adalah praktik yang sangat buruk.
Tim
@ Georgi-tolong peduli tentang berbicara lebih banyak tentang apa yang buruk.
Shivprasad Koirala
3
Maaf, kesalahan saya, saya seharusnya mengklarifikasi. Maksud saya penciptaan Tugas dalam satu lingkaran ke 1000000000. Biaya overhead tidak dapat dibayangkan. Belum lagi Paralel tidak dapat membuat lebih dari 63 tugas sekaligus, yang membuatnya jauh lebih optimal dalam kasus ini.
Georgi-it
Ini berlaku untuk 10.000.000 tugas. Namun ketika saya memproses gambar (berulang kali, zoom fraktal) dan melakukan Paralel. Karena pada baris banyak core yang menganggur sambil menunggu utas terakhir selesai. Untuk membuatnya lebih cepat saya membagi data sendiri menjadi 64 paket kerja dan membuat tugas untuk itu. (Kemudian Tugas. Tunggu Semua untuk menunggu penyelesaian.) Idenya adalah memiliki utas menganggur mengambil paket kerja untuk membantu menyelesaikan pekerjaan alih-alih menunggu 1-2 utas untuk menyelesaikan potongan (Parallel.For) yang ditugaskan.
Tedd Hansen
1
Apa yang Mehthod1()dilakukan dalam contoh ini?
Zapnologica
17

Parallel.ForEach akan mengoptimalkan (bahkan mungkin tidak memulai utas baru) dan memblokir sampai loop selesai, dan Task.Factory akan secara eksplisit membuat instance tugas baru untuk setiap item, dan kembali sebelum selesai (tugas tidak sinkron). Parallel.Foreach jauh lebih efisien.

Sogger
sumber
11

Dalam pandangan saya, skenario yang paling realistis adalah ketika tugas harus diselesaikan dengan berat. Pendekatan Shivprasad lebih berfokus pada penciptaan objek / alokasi memori daripada pada komputasi itu sendiri. Saya melakukan penelitian dengan memanggil metode berikut:

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

Eksekusi metode ini memakan waktu sekitar 0,5 detik.

Saya menyebutnya 200 kali menggunakan Paralel:

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

Lalu saya menyebutnya 200 kali menggunakan cara kuno:

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

Kasus pertama selesai dalam 26656ms, yang kedua dalam 24478ms. Saya mengulanginya berkali-kali. Setiap kali pendekatan kedua marjinal lebih cepat.

pengguna1089583
sumber
Menggunakan Paralel. Untuk adalah cara kuno. Disarankan menggunakan Task untuk unit kerja yang tidak seragam. Microsoft MVP dan desainer TPL juga menyebutkan bahwa menggunakan Tasks akan menggunakan utas lebih efisien, ienot memblokir sebanyak mungkin sambil menunggu unit lain selesai.
Suncat2000