Bagikan melalui


Potensi Perangkap dalam Paralelisme Data dan Tugas

Dalam banyak kasus, Parallel.For dan Parallel.ForEach dapat memberikan peningkatan performa yang signifikan atas perulangan berurutan biasa. 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 perulangan paralel.

Jangan Berasumsi Bahwa Paralel Selalu Lebih Cepat

Dalam kasus tertentu perulangan paralel mungkin berjalan lebih lambat dari setara berurutannya. Aturan dasar praktis adalah bahwa kueri yang memiliki beberapa elemen sumber dan delegasi pengguna yang cepat tidak mungkin mempercepat. Namun, karena banyak faktor yang terlibat dalam performa, kami sarankan Anda selalu mengukur hasil aktual.

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 perulangan paralel sebanyak mungkin. Cara terbaik untuk melakukan ini adalah dengan menggunakan kelebihan beban Parallel.For dan Parallel.ForEach yang menggunakan System.Threading.ThreadLocal<T> variabel untuk menyimpan status thread-local selama eksekusi perulangan. Untuk informasi selengkapnya, lihat Cara: Menulis Perulangan Paralel.For Loop dengan Variabel Rangkaian Lokal dan Cara: Menulis Perulangan Parallel.ForEach dengan Variabel Partisi-Lokal.

Hindari Paralelisasi Berlebihan

Dengan menggunakan perulangan paralel, Anda dikenakan biaya overhead untuk mempartisi koleksi sumber dan menyinkronkan utas pekerja. Manfaat paralelisasi dibatasi lebih lanjut oleh jumlah prosesor pada komputer. Tidak ada percepatan yang akan diperoleh dengan menjalankan beberapa utas terikat komputasi hanya pada satu prosesor. Oleh karena itu, Anda harus berhati-hati agar tidak melakukan paralelisasi perulangan yang berlebihan.

Skenario paling umum di mana paralelisasi berlebihan dapat terjadi adalah dalam perulangan berlapis. Dalam hal ini, yang terbaik adalah hanya menyejajarkan sumber data luar (pelanggan) kecuali satu atau beberapa kondisi berikut berlaku:

  • Perulangan dalam dikenal 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 paralelisasi pemrosesan.

Dalam semua kasus, cara terbaik untuk menentukan bentuk kueri optimal adalah dengan menguji dan mengukur.

Hindari Panggilan ke Metode Non-Thread-Safe

Menulis ke metode instans non-thread-safe dari perulangan paralel dapat menyebabkan kerusakan data yang mungkin atau mungkin tidak terdeteksi dalam program Anda. Ini juga dapat menyebabkan pengecualian. Dalam contoh berikut, beberapa utas akan mencoba memanggil metode FileStream.WriteByte secara bersamaan, yang tidak didukung oleh kelas.

FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
Dim fs As FileStream = File.OpenWrite(filepath)
Dim bytes() As Byte
ReDim bytes(1000000)
' ...init byte array
Parallel.For(0, bytes.Length, Sub(n) fs.WriteByte(bytes(n)))

Batasi Panggilan ke Metode Thread-Safe

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

Catatan

Anda dapat mengujinya sendiri dengan menyisipkan beberapa panggilan ke WriteLine dalam kueri Anda. Meski metode ini digunakan dalam contoh dokumentasi untuk tujuan demonstrasi, jangan gunakan dalam perulangan palalel kecuali dibutuhkan.

Waspadai Masalah Afinitas Utas

Beberapa teknologi, misalnya, interoperabilitas COM untuk komponen Single-Threaded Apartment (STA), Formulir Windows, dan Windows Presentation Foundation (WPF), memberlakukan pembatasan afinitas utas yang memerlukan kode untuk berjalan pada utas tertentu. Misalnya, di Formulir Windows dan WPF, kontrol hanya dapat diakses pada utas tempat kontrol dibuat. Ini berarti, misalnya, bahwa Anda tidak dapat memperbarui kontrol daftar dari perulangan paralel kecuali Anda mengonfigurasi penjadwal utas untuk menjadwalkan pekerjaan hanya pada utas UI. Untuk informasi selengkapnya, lihat Menentukan konteks sinkronisasi.

Berhati-hatilah Saat Menunggu di Delegasi yang Dipanggil oleh Paralel.Invoke

Dalam keadaan tertentu, Pustaka Paralel Tugas akan menggariskan tugas, yang berarti berjalan pada tugas pada utas yang sedang dijalankan. (Untuk informasi selengkapnya, lihat Penjadwal Tugas.) Pengoptimalan performa ini dapat menyebabkan kebuntuan dalam kasus tertentu. Misalnya, dua tugas mungkin menjalankan kode delegasi yang sama, yang memberi sinyal saat peristiwa terjadi, lalu menunggu tugas lain memberi sinyal. Jika tugas kedua disejajarkan pada utas yang sama dengan yang pertama, dan yang pertama masuk ke status Tunggu, tugas kedua tidak akan pernah dapat memberi sinyal peristiwanya. Untuk menghindari kemunculan seperti itu, Anda dapat menentukan batas waktu pada operasi Tunggu, atau menggunakan konstruktor utas eksplisit untuk membantu memastikan bahwa satu tugas tidak dapat memblokir yang lain.

Jangan berasumsi bahwa Iterasi ForEach, For, dan ForAll Selalu Dijalankan Secara Paralel

Penting untuk diingat bahwa perulangan individu dalam perulangan For, ForEach, atau ForAll mungkin tetapi tidak harus dijalankan secara paralel. Oleh karena itu, Anda harus menghindari penulisan kode apa pun yang bergantung pada kebenaran pada eksekusi paralel iterasi atau pada eksekusi iterasi dalam urutan tertentu. Misalnya, kode ini kemungkinan akan mengalami kebuntuan:

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

Dalam contoh ini, satu perulangan menetapkan peristiwa, dan semua iterasi lainnya menunggu 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 dari perulangan untuk membuat kemajuan. Jika perulangan paralel memutuskan untuk menjadwalkan iterasi secara berurutan tetapi dalam urutan yang berlawanan, kebuntuan akan terjadi.

Hindari Menjalankan Perulangan Paralel pada Utas UI

Penting untuk menjaga antarmuka pengguna (UI) aplikasi Anda tetap responsif. Jika operasi berisi pekerjaan yang cukup untuk menjamin paralelisasi, kemungkinan tidak boleh dijalankan pada utas UI. Sebaliknya, operasi tersebut harus dilepaskan untuk dijalankan pada utas latar belakang. Misalnya, jika Anda ingin menggunakan perulangan paralel untuk menghitung beberapa data yang kemudian harus dirender ke dalam kontrol UI, Anda harus mempertimbangkan untuk mengeksekusi perulangan dalam instans tugas daripada langsung di penanganan aktivitas UI. Hanya saat komputasi inti telah selesai jika Anda kemudian marshal pembaruan UI kembali ke utas UI.

Jika Anda menjalankan perulangan paralel pada utas UI, berhati-hatilah untuk menghindari pembaruan kontrol UI dari dalam perulangan. Mencoba memperbarui kontrol UI dari dalam perulangan paralel yang dijalankan pada utas UI dapat menyebabkan kerusakan status, pengecualian, pembaruan yang tertunda, dan bahkan kebuntuan, tergantung pada bagaimana pembaruan UI dipanggil. Dalam contoh berikut, perulangan paralel memblokir utas UI tempat utas dijalankan hingga semua perulangan selesai. Namun, jika perulangan perulangan berjalan pada utas latar belakang (seperti For yang mungkin dilakukan), panggilan ke Panggil menyebabkan pesan dikirimkan ke utas UI dan memblokir menunggu pesan tersebut diproses. Karena utas UI diblokir menjalankan For, pesan tidak pernah dapat diproses, dan kebuntuan utas UI.

private void button1_Click(object sender, EventArgs e)
{
    Parallel.For(0, N, i =>
    {
        // do work for i
        button1.Invoke((Action)delegate { DisplayProgress(i); });
    });
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Parallel.For(0, iterations, Sub(x)
                                    Button1.Invoke(Sub()
                                                       DisplayProgress(x)
                                                   End Sub)
                                End Sub)
End Sub

Contoh berikut menunjukkan cara menghindari kebuntuan, dengan menjalankan perulangan di dalam instans tugas. Utas UI tidak diblokir oleh perulangan, dan pesan dapat diproses.

private void button2_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(() =>
        Parallel.For(0, N, i =>
        {
            // do work for i
            button1.Invoke((Action)delegate { DisplayProgress(i); });
        })
         );
}
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Task.Factory.StartNew(Sub() Parallel.For(0, iterations, Sub(x)
                                                                Button1.Invoke(Sub()
                                                                                   DisplayProgress(x)
                                                                               End Sub)
                                                            End Sub))
End Sub

Lihat juga