Bahasa

Potensi perangkap dengan PLINQ

Dalam banyak kasus, PLINQ dapat memberikan peningkatan performa yang signifikan atas kueri LINQ berurutan ke Objek. Namun, pekerjaan paralelisasi eksekusi kueri memperkenalkan kompleksitas yang dapat menyebabkan masalah yang, dalam kode berurutan, tidak umum atau tidak ditemui sama sekali. Topik ini mencantumkan beberapa praktik yang harus dihindari saat Anda menulis kueri PLINQ.

Jangan berasumsi bahwa paralel selalu lebih cepat

Paralelisasi terkadang menyebabkan kueri PLINQ berjalan lebih lambat dibandingkan LINQ to Objects yang setara. Aturan dasar praktis adalah bahwa kueri yang memiliki beberapa elemen sumber dan delegasi pengguna yang cepat tidak akan mengalami peningkatan kecepatan yang signifikan. Namun, karena banyak faktor terlibat dalam performa, kami sarankan Anda mengukur hasil aktual sebelum Anda memutuskan apakah akan menggunakan PLINQ. Untuk informasi selengkapnya, lihat Memahami Speedup di PLINQ.

Hindari menulis ke lokasi memori bersama

Dalam kode berurutan, tidak jarang membaca dari atau menulis ke variabel statis atau bidang kelas. Namun, setiap kali beberapa utas mengakses variabel tersebut secara bersamaan, ada potensi besar untuk kondisi balapan. Meskipun Anda dapat menggunakan kunci untuk menyinkronkan akses ke variabel, biaya sinkronisasi dapat merusak performa. Oleh karena itu, kami sarankan Anda menghindari, atau setidaknya membatasi, akses ke status bersama dalam kueri PLINQ sebanyak mungkin.

Contoh: Kondisi lomba dengan memori bersama

Contoh berikut menunjukkan kondisi kompetisi yang terjadi ketika banyak utas mencoba menulis ke variabel bersama. Variabel total diakses dan dimodifikasi secara bersamaan oleh beberapa utas tanpa sinkronisasi, yang mengarah ke hasil yang tidak dapat diprediksi:

static void DemonstrateRaceCondition()
{
    int total = 0;
    var numbers = Enumerable.Range(0, 10000);

    // UNSAFE: Multiple threads writing to shared variable
    numbers.AsParallel().ForAll(n => total += n);

    Console.WriteLine($"Total (with race condition): {total}");
    // Expected: 49,995,000 but result is unpredictable due to race condition
}
Shared Sub DemonstrateRaceCondition()
    Dim total As Integer = 0
    Dim numbers = Enumerable.Range(0, 10000)

    ' UNSAFE: Multiple threads writing to shared variable
    numbers.AsParallel().ForAll(Sub(n) total += n)

    Console.WriteLine($"Total (with race condition): {total}")
    ' Expected: 49,995,000 but result is unpredictable due to race condition
End Sub

Dalam kode ini, operasi total += n tidak atomik. Ini melibatkan membaca nilai saat ini dari total, menambahkan n, dan menulis hasilnya kembali ke total. Ketika beberapa utas menjalankan operasi ini secara bersamaan, mereka dapat membaca nilai yang sama, menambahkannya di setiap utas, dan menulis kembali hasil yang saling menimpa. Ini menyebabkan beberapa penambahan hilang, menghasilkan hasil akhir yang salah.

Pendekatan yang benar adalah menggunakan operasi yang aman untuk utas yang tidak memerlukan status bersama yang dapat diubah.

static void DemonstrateCorrectApproach()
{
    var numbers = Enumerable.Range(0, 10000);

    // SAFE: Use thread-safe aggregate operation
    int total = numbers.AsParallel().Sum();

    Console.WriteLine($"Total (correct): {total}");
    // Result is always 49,995,000
}
Shared Sub DemonstrateCorrectApproach()
    Dim numbers = Enumerable.Range(0, 10000)

    ' SAFE: Use thread-safe aggregate operation
    Dim total As Integer = numbers.AsParallel().Sum()

    Console.WriteLine($"Total (correct): {total}")
    ' Result is always 49,995,000
End Sub

Metode ini Sum mengelola paralelisasi secara internal dengan thread-safe, memastikan hasil yang benar tanpa perlu sinkronisasi eksplisit. Pendekatan aman lainnya termasuk menggunakan Aggregate untuk agregasi khusus atau mengumpulkan hasil dalam koleksi thread-safe seperti ConcurrentBag<T>.

Hindari paralelisasi berlebihan

Dengan menggunakan metode AsParallel, Anda akan dikenakan biaya overhead untuk mempartisi koleksi sumber dan menyinkronkan utas pekerja. Manfaat paralelisasi dibatasi lebih lanjut oleh jumlah prosesor pada komputer. Tidak ada peningkatan kecepatan yang dapat diperoleh dengan menjalankan beberapa utas yang terikat komputasi hanya pada satu prosesor. Oleh karena itu, Anda harus berhati-hati agar tidak membuat kueri terlalu paralel dalam pemrosesannya.

Skenario paling umum di mana paralelisasi berlebihan dapat terjadi adalah dalam kueri berlapis, seperti yang ditunjukkan pada cuplikan berikut.

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };
Dim q = From cust In customers.AsParallel()
        From order In cust.Orders.AsParallel()
        Where order.OrderDate > aDate
        Select New With {cust, order}

Dalam hal ini, yang terbaik adalah hanya menyejajarkan sumber data luar (pelanggan) kecuali satu atau beberapa kondisi berikut berlaku:

  • Sumber data dalam (cust.Orders) diketahui sangat panjang.

  • Anda melakukan komputasi mahal pada setiap pesanan. (Operasi yang ditunjukkan dalam contoh tidak mahal.)

  • Sistem target diketahui memiliki prosesor yang cukup untuk menangani jumlah utas yang akan diproduksi dengan memparalelkan kueri pada cust.Orders.

Dalam semua kasus, cara terbaik untuk menentukan bentuk kueri optimal adalah dengan menguji dan mengukur. Untuk informasi selengkapnya, lihat Cara: Mengukur Performa Kueri PLINQ.

Hindari panggilan ke metode yang tidak thread-safe

Menulis ke metode instance yang tidak aman untuk thread dari kueri PLINQ dapat menyebabkan kerusakan data yang mungkin tidak terdeteksi dalam program Anda. Ini juga dapat menyebabkan pengecualian. Dalam contoh berikut, beberapa utas akan mencoba memanggil metode FileStream.Write secara bersamaan, yang tidak didukung oleh class tersebut.

Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

Pembatasan panggilan ke metode thread-safe

Sebagian besar metode statis dalam .NET aman untuk thread dan dapat dipanggil dari beberapa utas secara bersamaan. Namun, bahkan dalam kasus ini, sinkronisasi yang terlibat dapat menyebabkan perlambatan yang signifikan dalam kueri.

Nota

Anda dapat mengujinya sendiri dengan menyisipkan beberapa panggilan ke WriteLine dalam kueri Anda. Meskipun metode ini digunakan dalam contoh dokumentasi untuk tujuan demonstrasi, jangan gunakan dalam kueri PLINQ.

Hindari operasi pemesanan yang tidak perlu

Ketika PLINQ menjalankan kueri secara paralel, PLINQ membagi urutan sumber menjadi partisi yang dapat dioperasikan secara bersamaan pada beberapa utas. Secara default, urutan di mana partisi diproses dan hasilnya dikirimkan tidak dapat diprediksi (kecuali untuk operator seperti OrderBy). Anda dapat menginstruksikan PLINQ untuk mempertahankan urutan sumber apa pun, tetapi ini berdampak negatif pada performa. Praktik terbaik, jika memungkinkan, adalah menyusun kueri sehingga tidak bergantung pada pelestarian pesanan. Untuk informasi selengkapnya, lihat Pelestarian Pesanan di PLINQ.

Lebih suka ForAll ke ForEach jika memungkinkan

Meskipun PLINQ menjalankan kueri pada beberapa utas, jika Anda menggunakan hasilnya dalam perulangan foreach (For Each dalam Visual Basic), maka hasil kueri harus digabungkan kembali ke dalam satu utas dan diakses secara serial oleh enumerator. Dalam beberapa kasus, ini tidak dapat dihindari; namun, jika memungkinkan, gunakan metode ForAll untuk memungkinkan setiap utas menghasilkan hasilnya sendiri, misalnya, dengan menulis ke suatu koleksi aman utas seperti System.Collections.Concurrent.ConcurrentBag<T>.

Masalah yang sama berlaku untuk Parallel.ForEach. Dengan kata lain, source.AsParallel().Where().ForAll(...) harus sangat disukai daripada Parallel.ForEach(source.AsParallel().Where(), ...).

Waspadai masalah afinitas utas

Beberapa teknologi, misalnya, interoperabilitas COM untuk komponen Single-Threaded Apartment (STA), Windows Forms, dan Windows Presentation Foundation (WPF), memberlakukan pembatasan afinitas utas yang memerlukan kode untuk berjalan pada utas tertentu. Misalnya, dalam Windows Forms dan WPF, kontrol hanya dapat diakses pada utas tempat kontrol dibuat. Jika Anda mencoba mengakses status bersama kontrol Windows Forms dalam kueri PLINQ, pengecualian dinaikkan jika Anda menjalankan di debugger. (Pengaturan ini dapat dimatikan.) Namun, jika kueri Anda digunakan pada utas UI, maka Anda dapat mengakses kontrol dari foreach perulangan yang menghitung hasil kueri karena kode tersebut dijalankan hanya pada satu utas.

Jangan berasumsi bahwa iterasi ForEach, For, dan ForAll selalu dijalankan secara paralel

Penting untuk diingat bahwa iterasi individu dalam Parallel.For, Parallel.ForEach atau ForAll dapat tetapi tidak harus dijalankan secara paralel. Oleh karena itu, Anda harus menghindari penulisan kode apa pun yang tergantung pada kebenaran pada eksekusi iterasi paralel atau pada eksekusi iterasi dalam urutan tertentu.

Misalnya, kode ini kemungkinan akan kebuntuan:

Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll(Sub(j)
   If j = Environment.ProcessorCount Then
       Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Set()
   Else
       Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Wait()
   End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll((j) =>
{
    if (j == Environment.ProcessorCount)
    {
        Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Set();
    }
    else
    {
        Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Wait();
    }
}); //deadlocks

Dalam contoh ini, satu iterasi mengatur peristiwa, dan semua iterasi lainnya menunggu pada peristiwa. Tidak ada perulangan menunggu yang dapat diselesaikan hingga iterasi pengaturan peristiwa selesai. Namun, ada kemungkinan bahwa iterasi menunggu memblokir semua utas yang digunakan untuk menjalankan perulangan paralel, sebelum iterasi pengaturan peristiwa memiliki kesempatan untuk dijalankan. Ini menghasilkan kebuntuan - iterasi pengaturan peristiwa tidak akan pernah dijalankan, dan iterasi menunggu tidak akan pernah bangun.

Secara khusus, satu iterasi perulangan paralel tidak boleh menunggu iterasi lain untuk berkembang. Jika perulangan paralel memutuskan untuk menjadwalkan iterasi secara berurutan tetapi dalam urutan yang berlawanan, kebuntuan akan terjadi.

Lihat juga