Dengan munculnya CPU multicore dalam beberapa tahun terakhir, pemrograman paralel adalah cara untuk memanfaatkan sepenuhnya kuda kerja pemrosesan baru. Pemrograman paralel mengacu pada eksekusi proses secara bersamaan karena ketersediaan beberapa inti pemrosesan. Ini, pada dasarnya, mengarah pada peningkatan luar biasa dalam kinerja dan efisiensi program berbeda dengan eksekusi inti tunggal linier atau bahkan multithreading. Kerangka Fork/Join adalah bagian dari Java concurrency API. Kerangka kerja ini memungkinkan programmer untuk memparalelkan algoritma. Artikel ini mengeksplorasi konsep pemrograman paralel dengan bantuan Fork/Join Framework yang tersedia di Java.
Ikhtisar
Pemrograman paralel memiliki konotasi yang jauh lebih luas dan tidak diragukan lagi merupakan area yang luas untuk diuraikan dalam beberapa baris. Inti masalahnya cukup sederhana, namun secara operasional jauh lebih sulit untuk dicapai. Secara sederhana, pemrograman paralel berarti menulis program yang menggunakan lebih dari satu prosesor untuk menyelesaikan tugas, itu saja! Tebak apa; kedengarannya akrab, bukan? Ini hampir berima dengan gagasan multithreading. Tapi, perhatikan bahwa ada beberapa perbedaan penting di antara mereka. Di permukaan, mereka sama, tetapi arus bawahnya benar-benar berbeda. Faktanya, multithreading diperkenalkan untuk memberikan semacam ilusi pemrosesan paralel tanpa eksekusi paralel nyata sama sekali. Apa yang sebenarnya dilakukan multithreading adalah mencuri waktu idle CPU dan menggunakannya untuk keuntungannya.
Singkatnya, multithreading adalah kumpulan unit logis diskrit dari tugas yang dijalankan untuk mengambil bagian waktu CPU mereka sementara utas lain mungkin sementara menunggu, katakanlah, beberapa input pengguna. Waktu CPU yang menganggur dibagikan secara optimal di antara utas yang bersaing. Jika hanya ada satu CPU, itu adalah waktu bersama. Jika ada beberapa inti CPU, mereka juga dibagikan sepanjang waktu. Oleh karena itu, program multithreaded yang optimal memeras kinerja CPU dengan mekanisme pembagian waktu yang cerdas. Intinya, selalu satu utas menggunakan satu CPU sementara utas lain menunggu. Ini terjadi secara halus sehingga pengguna merasakan pemrosesan paralel di mana, pada kenyataannya, pemrosesan sebenarnya terjadi secara berurutan. Keuntungan terbesar dari multithreading adalah bahwa itu adalah teknik untuk mendapatkan hasil maksimal dari sumber daya pemrosesan. Sekarang, ide ini cukup berguna dan dapat digunakan di lingkungan apa pun, baik yang memiliki satu CPU atau banyak CPU. Idenya sama.
Pemrograman paralel, di sisi lain, berarti bahwa ada beberapa CPU khusus yang dimanfaatkan secara paralel oleh programmer. Jenis pemrograman ini dioptimalkan untuk lingkungan CPU multicore. Sebagian besar mesin saat ini menggunakan CPU multicore. Oleh karena itu, pemrograman paralel cukup relevan saat ini. Bahkan mesin yang paling murah pun dipasang dengan CPU multicore. Lihatlah perangkat genggam; bahkan mereka multicore. Meskipun semuanya tampak keren dengan CPU multicore, ini juga sisi lain dari cerita. Apakah lebih banyak inti CPU berarti komputasi yang lebih cepat atau efisien? Tidak selalu! Filosofi serakah "semakin banyak semakin meriah" tidak berlaku untuk komputasi, atau dalam kehidupan. Tapi mereka ada di sana, tanpa disadari—dual, quad, octa, dan seterusnya. Mereka ada di sana sebagian besar karena kita menginginkannya dan bukan karena kita membutuhkannya, setidaknya dalam banyak kasus. Pada kenyataannya, relatif sulit untuk membuat satu CPU tetap sibuk dalam komputasi sehari-hari. Namun, multicore memiliki kegunaannya dalam keadaan khusus, seperti di server, game, dan sebagainya, atau memecahkan masalah besar. Masalah memiliki banyak CPU adalah membutuhkan memori yang harus menyesuaikan kecepatan dengan kekuatan pemrosesan, bersama dengan saluran data secepat kilat dan aksesori lainnya. Singkatnya, beberapa inti CPU dalam komputasi harian memberikan peningkatan kinerja yang tidak dapat melebihi jumlah sumber daya yang dibutuhkan untuk menggunakannya. Akibatnya, kami mendapatkan mesin mahal yang kurang dimanfaatkan, mungkin hanya dimaksudkan untuk dipamerkan.
Pemrograman Paralel
Tidak seperti multithreading, di mana setiap tugas adalah unit logis diskrit dari tugas yang lebih besar, tugas pemrograman paralel bersifat independen dan urutan eksekusinya tidak masalah. Tugas didefinisikan sesuai dengan fungsi yang mereka lakukan atau data yang digunakan dalam pemrosesan; ini disebut paralelisme fungsional atau paralelisme data , masing-masing. Dalam paralelisme fungsional, setiap prosesor bekerja pada bagian masalahnya sedangkan dalam paralelisme data, prosesor bekerja pada bagian datanya. Pemrograman paralel cocok untuk basis masalah yang lebih besar yang tidak cocok dengan arsitektur CPU tunggal, atau mungkin masalahnya begitu besar sehingga tidak dapat diselesaikan dalam perkiraan waktu yang wajar. Akibatnya, tugas, ketika didistribusikan di antara prosesor, dapat memperoleh hasil yang relatif cepat.
Kerangka Fork/Gabung
Fork/Join Framework didefinisikan di java.util.concurrent kemasan. Ini mencakup beberapa kelas dan antarmuka yang mendukung pemrograman paralel. Apa yang dilakukannya terutama adalah menyederhanakan proses pembuatan beberapa utas, penggunaannya, dan mengotomatiskan mekanisme alokasi proses di antara banyak prosesor. Perbedaan mencolok antara multithreading dan pemrograman paralel dengan kerangka kerja ini sangat mirip dengan apa yang kami sebutkan sebelumnya. Di sini, bagian pemrosesan dioptimalkan untuk menggunakan beberapa prosesor tidak seperti multithreading, di mana waktu idle dari satu CPU dioptimalkan berdasarkan waktu bersama. Keuntungan tambahan dengan kerangka kerja ini adalah menggunakan multithreading dalam lingkungan eksekusi paralel. Tidak ada salahnya.
Ada empat kelas inti dalam kerangka kerja ini:
- ForkJoinTask
: Ini adalah kelas abstrak yang mendefinisikan tugas. Biasanya, tugas dibuat dengan bantuan fork() metode yang didefinisikan di kelas ini. Tugas ini hampir mirip dengan utas biasa yang dibuat dengan Utas kelas, tetapi lebih ringan dari itu. Mekanisme yang diterapkan adalah memungkinkan pengelolaan sejumlah besar tugas dengan bantuan sejumlah kecil utas aktual yang bergabung dengan ForkJoinPool . garpu() metode memungkinkan eksekusi asinkron dari tugas pemanggilan. bergabung() metode memungkinkan menunggu sampai tugas yang dipanggil akhirnya dihentikan. Ada metode lain, yang disebut invoke() , yang menggabungkan garpu dan bergabung operasi menjadi satu panggilan. - ForkJoinPool: Kelas ini menyediakan kumpulan umum untuk mengelola eksekusi ForkJoinTask tugas. Ini pada dasarnya menyediakan titik masuk untuk pengiriman dari non-ForkJoinTask klien, serta operasi manajemen dan pemantauan.
- Tindakan Rekursif: Ini juga merupakan ekstensi abstrak dari ForkJoinTask kelas. Biasanya, kami memperluas kelas ini untuk membuat tugas yang tidak mengembalikan hasil atau memiliki void tipe pengembalian. komputasi() metode yang didefinisikan dalam kelas ini diganti untuk memasukkan kode komputasi tugas.
- Tugas Rekursif
: Ini adalah ekstensi abstrak lain dari ForkJoinTask kelas. Kami memperluas kelas ini untuk membuat tugas yang mengembalikan hasil. Dan, mirip dengan ResursiveAction, ini juga menyertakan protected abstract compute() metode. Metode ini diganti untuk memasukkan bagian komputasi dari tugas.
Strategi Fork/Join Framework
Kerangka kerja ini menggunakan divide-and-conquer . rekursif strategi untuk menerapkan pemrosesan paralel. Ini pada dasarnya membagi tugas menjadi subtugas yang lebih kecil; kemudian, setiap subtugas dibagi lagi menjadi sub-subtugas. Proses ini diterapkan secara rekursif pada setiap tugas sampai cukup kecil untuk ditangani secara berurutan. Misalkan kita akan menaikkan nilai array N angka. Ini adalah tugas. Sekarang, kita dapat membagi array dengan dua membuat dua subtugas. Bagi masing-masing lagi menjadi dua subtugas lagi, dan seterusnya. Dengan cara ini, kita dapat menerapkan divide-and-conquer strategi secara rekursif sampai tugas-tugas dipilih menjadi masalah unit. Masalah unit ini kemudian dapat dieksekusi secara paralel oleh beberapa prosesor inti yang tersedia. Dalam lingkungan non-paralel, yang harus kami lakukan adalah menggilir seluruh array dan melakukan pemrosesan secara berurutan. Ini jelas merupakan pendekatan yang tidak efisien mengingat pemrosesan paralel. Tapi, pertanyaan sebenarnya adalah bisakah setiap masalah dibagi dan ditaklukkan ? Pasti TIDAK! Tapi, ada masalah yang sering melibatkan semacam array, pengumpulan, pengelompokan data yang sangat sesuai dengan pendekatan ini. Omong-omong, ada masalah yang mungkin tidak menggunakan pengumpulan data namun dapat dioptimalkan untuk menggunakan strategi pemrograman paralel. Jenis masalah komputasi apa yang cocok untuk pemrosesan paralel atau diskusi tentang algoritma paralel di luar cakupan artikel ini. Mari kita lihat contoh singkat tentang penerapan Fork/Join Framework.
Contoh Singkat
Ini adalah contoh yang sangat sederhana untuk memberi Anda gambaran tentang bagaimana menerapkan paralelisme di Java dengan Fork/Join Framework.
package org.mano.example; import java.util.concurrent.RecursiveAction; public class CustomRecursiveAction extends RecursiveAction { final int THRESHOLD = 2; double [] numbers; int indexStart, indexLast; CustomRecursiveAction(double [] n, int s, int l) { numbers = n; indexStart = s; indexLast = l; } @Override protected void compute() { if ((indexLast - indexStart) > THRESHOLD) for (int i = indexStart; i < indexLast; i++) numbers [i] = numbers [i] + Math.random(); else invokeAll (new CustomRecursiveAction(numbers, indexStart, (indexStart - indexLast) / 2), new CustomRecursiveAction(numbers, (indexStart - indexLast) / 2, indexLast)); } } package org.mano.example; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) { final int SIZE = 10; ForkJoinPool pool = new ForkJoinPool(); double na[] = new double [SIZE]; System.out.println("initialized random values :"); for (int i = 0; i < na.length; i++) { na[i] = (double) i + Math.random(); System.out.format("%.4f ", na[i]); } System.out.println(); CustomRecursiveAction task = new CustomRecursiveAction(na, 0, na.length); pool.invoke(task); System.out.println("Changed values :"); for (inti = 0; i < 10; i++) System.out.format("%.4f ", na[i]); System.out.println(); } }
Kesimpulan
Ini adalah deskripsi singkat tentang pemrograman paralel dan bagaimana hal itu didukung di Java. Ini adalah fakta yang mapan bahwa memiliki N core tidak akan membuat semuanya N kali lebih cepat. Hanya sebagian dari aplikasi Java yang menggunakan fitur ini secara efektif. Kode pemrograman paralel adalah kerangka yang sulit. Selain itu, program paralel yang efektif harus mempertimbangkan masalah seperti penyeimbangan beban, komunikasi antara tugas paralel, dan sejenisnya. Ada beberapa algoritma yang lebih sesuai dengan eksekusi paralel tetapi banyak yang tidak. Bagaimanapun, Java API tidak kekurangan dukungannya. Kami selalu dapat mengotak-atik API untuk mencari tahu mana yang paling cocok. Selamat membuat kode