Multithreading Di Groovy


Karena Groovy dapat mengakses class Java, maka secara tidak langsung, Groovy juga mendukung multithreading. Java 8 memiliki banyak fitur tambahan yang berkaitan dengan multithreading (concurrency), misalnya tambahan method forEach(), search(), reduce(), Array.parallelSort(), dan sebagainya. Groovy, tanpa Java 8, juga memiliki fasilitas yang serupa melalui GPars. Sebelumnya GPars adalah proyek terpisah tapi kini sudah menjadi bagian dari Groovy sehingga tidak perlu di-download secara terpisah lagi.

Sebagai latihan, saya akan membuat sebuah program yang mencari bilangan prima. Program ini akan memakai algoritma yang paling buruk dan paling lambat, tapi setidaknya sangat mudah dimengerti. Ini adalah versi kode program yang hanya berjalan pada thread tunggal:

List hasil = []

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

(2..100000).each { angka ->
    if (isPrimary(angka)) hasil << angka
}

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Kode program tersebut membutuhkan waktu sekitar 9.356.485.040 ns. Kode program tersebut hanya bekerja pada satu thread yaitu thread main yang terlihat seperti pada gambar hasil di JVisualVM berikut ini:

Kode program yang hanya dijalankan di thread main

Kode program yang hanya dijalankan di thread main

Thread dengan nama main adalah thread yang diciptakan secara otomatis pada saat kode program Java dikerjakan. Karena kode program di atas tidak membuat thread baru, maka hingga selesai dikerjakan, program yang saya buat di atas akan berjalan di thread main.

Pada Groovy, saya tetap dapat menggunakan instance dari Thread untuk mewakili sebuah thread baru. Akan tetapi, saya akan memakai cara yang lebih mudah dengan menggunakan task dari GPars. Sebagai contoh, saya akan mengubah kode program di atas agar membagi proses looping menjadi 2 bagian yang paralel, seperti berikut ini:

import javax.swing.JOptionPane
import java.util.concurrent.ConcurrentSkipListSet
import static groovyx.gpars.dataflow.Dataflow.task

ConcurrentSkipListSet hasil = new ConcurrentSkipListSet()

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (2..50000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t2 = task {
    (50001..100000).each {
        if (isPrimary(it)) hasil << it
    }
}

[t1,t2]*.join()

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Kode program di atas membutuhkan waktu sekitar 7.585.801.575 ns atau lebih cepat 19% dibanding versi thread tunggal sebelumnya. Pada saat program ini dijalankan, t1 dan t2 masing-masing akan dikerjakan pada thread terpisah dengan nama Actor Thread 1 dan Actor Thread 2 seperti yang terlihat pada gambar hasil JVisualVM berikut ini:

Kode program yang dijalankan pada 2 thread

Kode program yang dijalankan pada 2 thread

Terlihat bahwa thread main kini menghabiskan lebih banyak waktunya untuk menunggu. Actor Thread 2 bekerja lebih keras sementara Actor Thread 1 lebih duluan selesai. Walaupun sudah selesai, Actor Thread 1 harus menunggu hingga thread lain selesai, karena saya memanggil join() dengan [t1,t2]*.join(). Thread yang nganggur adalah pemborosan, oleh sebab itu Java 7 dilengkapi dengan fork/join framework yang memiliki algoritma work-stealing untuk mengurangi thread yang nganggur. Saya tidak akan memakainya disini.

Saya akan mencoba meningkatkan jumlah task menjadi lebih banyak dengan mengubah kode program menjadi seperti berikut ini:

import javax.swing.JOptionPane
import java.util.concurrent.ConcurrentSkipListSet
import static groovyx.gpars.dataflow.Dataflow.task

ConcurrentSkipListSet hasil = new ConcurrentSkipListSet()

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (1..25000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t2 = task {
    (25001..50000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t3 = task {
    (50001..75000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t4 = task {
    (75001..100000).each {
        if (isPrimary(it)) hasil << it
    }
}

[t1,t2,t3,t4]*.join()

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Waktu eksekusi kini menjadi 5.899.754.797 atau lebih cepat 37% dibanding versi thread tunggal. Gambar hasil JVisualVM menunjukkan bahwa kini ada 4 thread yang bekerja secara paralel:

Kode program yang dijalankan pada 4 thread

Kode program yang dijalankan pada 4 thread

Terlihat pada thread yang memproses bilangan yang kecil akan lebih cepat selesai dan lebih banyak menunggu (warna kuning menunjukkan thread sedang nganggur). Hal ini karena bilangan kecil membutuhkan looping yang lebih singkat, sementara bilang besar membutuhkan lebih banyak looping.

Bila saya hanya ingin menampilkan bilangan prima yang ditemukan langsung pada layar, saya dapat membuat sebuah task baru seperti berikut ini:

def outputTask = task {
    while (true) {
        println hasil.last()
    }
}

Task di atas akan menampilkan hasil perhitungan terakhir. Tapi karena ia dijalankan tidak tersinkronisasi dengan task lainnya, maka sering kali nilai yang sama ditampilkan berulang kali. Groovy mengatasi hal ini dengan cara yang sangat sederhana yang disebut dataflow. Dengan dataflow, sebuah task menulis nilai, dan task lain akan mengerjakan sesuatu hanya jika nilai tersebut sudah terisi. Sebagai contoh, saya dapat menggunakan DataflowQueues seperti pada kode program berikut ini:

import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.task

DataflowQueue buffer = new DataflowQueue()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (2..25000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t2 = task {
    (25001..50000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t3 = task {
    (50001..75000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t4 = task {
    (75001..100000).each {
        if (isPrimary(it)) buffer << it
    }
}

def output = task {
    while (true) {
        println buffer.val
    }
}

[t1,t2,t3,t4]*.join()

Untuk mengisi nilai pada dataflow, saya menggunakan kode program seperti buffer << it. Untuk mengakses nilainya, saya menggunakan kode program seperti buffer.val yang akan menunggu hingga ada nilai yang dapat dibaca.

Perihal Solid Snake
I'm nothing...

One Response to Multithreading Di Groovy

  1. Ping-balik: Memakai Fork/Join Framework Di Groovy | The Solid Snake

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: