Kode program mutex

Golang – Race condition & package sync

Sekarang kita akan membahas salah satu masalah yang sering terjadi dalam concurency atau pararell programming yaitu race condition, saat kita menggunakan goroutine dia tidak hanya berjalan secara concurrent tetapi bisa pararell juga, karena bisa pararell maka akan ada beberapa thread yang berjalan secara pararell. Hal ini sangat berbahaya ketika kita melakukan manipulasi data variable yang sama atau istilah nya sharing variable, jadi ada satu variable yang diakses oleh beberapa goroutine di waktu yang sama. Hal ini bisa menyebabkan masalah yang namanya race condition

Kode : Race condition

Kode program race condition
Kode program race condition

Jadi kita memiliki variable x dengan nilai awal 0, lalu kita akan melakukan perulangan dari 1 – 1000, selanjutnya kita akan me running goroutine artinya kita akan me running 1000 goroutine lalu di dalam goroutine nya kita akan malakukan perulangan j dari 1 – 100, lalu kita akan melakukan increment atau menaikkan variable x sebanyak 1 setiap perulangan. Artinya setiap goroutine akan menaikkan sebanyak 100 dan karena kita merunning 1000 goroutine harusnya total akhir x nya adalah 100.000. Terakhir kita akan print x nya berapa dan juga tidak lupa kita sleep selama 5 detik untuk menunggu semua goroutine selesai.

package belajar_golang_goroutines

import (
	"fmt"
	"testing"
	"time"
)

func TestRaceCondition(t *testing.T) {
	x := 0

	for i := 1; i <= 1000; i++ {
		go func() {
			for j := 1; j <= 100; j++ {
				x = x + 1
			}
		}()
	}

	time.Sleep(5 * time.Second)
	fmt.Println("Counter = ", x)
}

Coba jalankan kode program diatas dan lihat hasilnya apakah Counter x nya berjumlah 100.000

Output kode program race condition (1)
Output kode program race condition (1)

Nah ternyata counter nya tidak berjumlah 100.000 tetapi hanya sekitar 98.000, apakah pasti jumlah nya 98.000 terus? coba lah running kembali program nya

Output kode program race condition (2)
Output kode program race condition (2)

Sekarang Counter x nya sudah buka 98.000 lagi tetapi 97.000, jadi setiap kita running hasil nya selalu berubah-ubah, kenapa hal ini bisa terjadi, hal ini karena kita melakukan sharring variable x ke banyak goroutine atau di contoh kali ini kita melakukan sharing ke 1000 goroutine. Dan karena goroutine bisa berjalan secara pararell maka akan ada saat dimana bebrapa goroutine dengan waktu yang sama persis melakukan counter ke variable x, contoh nya ada 2 goroutine yang mengakses x di waktu yang sama dimana saat itu nilai x 1000 artinya dua goroutine itu akan melakukan 1000 + 1 di saat bersamaan.

Hasilnya terdapat beberapa nilai yang hilang karena memang dilakukan penjumlahan di saat yang sama pada satu variable. Inilah bahaya saat kita menggunakan concurentcy karena akan terjadi race condition atau bisa dibilang goroutine nya akan balapan merubah goroutine nya.

sync.Mutex (Mutual Exclusion)

Untuk mengatasi masalah race condition tersebut, di golang terdapat struct bernama sync.Mutex, mutex bisa digunakan untuk melakukan locking atau mengunci sebuah block kode program dan juga melakukan unlock atau istilahnya melepas kuncinya, dimana ketika kita melakukan locking atau mengunci terhadap mutex, maka tidak akan ada yang bisa melakukan locking lagi sampai kita melakukan unlock.

Jadi biasanya sebelum kita merubah variable yang di sharing ke beberapa goroutine, kita melakukan locking terhadap mutex, artinya setiap gorutine yang ingin mengakses harus melakukan locking lagi, dan saat kita melakukan locking maka mutex hanya mengizinkan satu goroutine untuk melakukan locking. Setelah kita melakukan locking dan selesai merubah variable nya maka kita melakukan unlock lagi, barulah goroutine selanjutnya diperbolehkan melakukan lock lagi.

Ini sangat cocok sebagai solusi ketika ada masalah race condition yang sebelumnya kita hadapi

Kode : Mutex

Kode program mutex
Kode program mutex

Kode diatas sama dengan race condition sebelumnya, bedanya disini kita membuat variable mutex pada baris ke 12, kemudian pada baris ke 17 sebelum dilakukan increment pada variable x kita lock terlebih dahulu mutex nya dan setalah dilakukan increment baru kita unlock kembali. Maka 1000 goroutine ini akan mencoba melakukan locking dan cuma satu goroutine saja yang diperbolehkan melakukan lock nya, setelah satu goroutine itu berhasil melakukan lock maka 999 goroutine lainnya akan menunggu sampai mock nya di unlock.

Memang kelihatannya akan memperlambat tetapi tidak akan terlalu lambat karena dalam hitungan nano second harsnya proses ini sudah selesai.

package belajar_golang_goroutines

import (
	"fmt"
	"sync"
	"testing"
	"time"
)

func TestMutex(t *testing.T) {
	x := 0
	var mutex sync.Mutex

	for i := 1; i <= 1000; i++ {
		go func() {
			for j := 1; j <= 100; j++ {
				mutex.Lock()
				x = x + 1
				mutex.Unlock()
			}
		}()
	}

	time.Sleep(5 * time.Second)
	fmt.Println("Counter =  ", x)
}

Seperti yang saya sampaikan sebelumnya jika kode program diatas sama dengan sebelumnya hanya saja kita menambahkan variable mutex dan melakukan locking & unlocking pada variable x, dengan begini saja kita sudah berhasil menghindari race condition. Jadi kapan kita menggunakan mutex, kita menggunakannya jika kita memiliki 1 variable yang di sharing ke beberapa goroutine.

Coba jalankan kode program diatas dan pastikan hasilnya 100.000

Output kode program mutex
Output kode program mutex

Bisa dilihat jika kita melakukan locking maka hasil nya benar menjadi 100.000

sync.RWMutex (Read Write Mutex)

Kadang ada kasus dimana kita ingin melakukan locking tidak hanya pada proses merubah data, tapi juga membaca data. Kita sebenar nya bisa menggunakan mutex saja namun nanti akan terjadi rebutan antara proses membaca dan mengubah. Di golang telah disediakan struct RWMutex(Read Write Mutex) untuk menangani hal ini, dimana mutex jenis ini memiliki dua lock, lock untuk Read dan lock untuk Write.

Kode : RWMutex

Kode program Read Write Mutex
Kode program Read Write Mutex

Perhatikan kode diatas, jadi misal kita memiliki struct BackAccount dimana didalamnya terdapat Balance, dan kita juga memiliki method AddBalance yang mana akan merubah data Balance, nah agar tidak terjadi race condition disini kita akan melakukan locking menggunakan RWMutex.

Selanjutnya kita juga punya method lagi dengan nama GetBalance untuk mendapatkan data Balance, lalu agar tidak terjadi race condition kita juga akan melakukan lock terhadap RWMutex dengan function RLock dan untuk unlock kita bisa menggunakan function RUnlock.

Kemudian untuk menjalankannya buat juga unit test yang didalamnya akan melakukan perulangan seperti sebelumnya, dimana kita akan membuat 100 goroutine dan di tiap-tiap goroutine akan melakukan 100 perulangan dan menambahkan 1 Balance, kemudian kita juga akan melakukan print setelah dilakukan penambahan Balance. Terakhir pada baris ke 41 kita akan melakukan print total Balance nya.

Coba jalankan program diatas dan pastikan hasilnya menjadi 10.000

Output kode program RWMutex
Output kode program RWMutex

Bisa dilihat pada output hasilnya sudah benar menjadi 10.000 dan tidak terjadi race condition.

sync.WaitGroup

WaitGroup adalah sebuah fitur yang bisa digunakan untuk menunggu sebuah proses selesai dilakukan, biasanya WaitGroup digunakan untuk menunggu proses goroutine selesai karena seperti yang kita tahu goroutine itu berjalan secara asynchronous, sebelumnya untuk menunggunya kita menggunakan Sleep tetapi hal ini sebenarnya tidak di rekomendasikan.

WaitGroup diperlukan saat kita ingin menjalankan beberapa proses goroutine, tapi kita ingin semua proses selesai terlebih dahulu sebelum aplikasi kita selesai. Untuk menandai bahwa ada proses goroutine, kita bisa menggunakan method Add(int), setelah proses goroutine selesai kita bisa menggunaklan method Done(), dan untuk menunggu semua proses selesai kita bisa menggunakan method Wait().

Kode : WaitGroup

Kode program wait group
Kode program wait group

Contoh diatas kita membuat func dengan nama RunAsynchronous pada baris ke 10 yang akan di running dengan goroutine, kemudian kita hanya perlu menambahkan parameter *sync.WaitGroup, untuk menambahkan proses gunakan method Add(int) dan dibawahnnya barulah masukkan kode program kita, jangan lupa juga untuk menandai proses telah selesai dengan method Done() menggunakan defer agar bisa dipastikan method ini akan dijalankan setelah function selesai.

Kemudian pada baris ke 19 kita membuat unit test kembali dan isinya, pertama kita buat group selanjutnya lakukan perulangan untuk memanggil 100 goroutine, jangan lupa juga kirimkan group ke parameter nya, selanjutnya pada baris 26 kita akan menunggu sampai proses selesai semua.

package belajar_golang_goroutines

import (
	"fmt"
	"sync"
	"testing"
	"time"
)

func RunAsynchronous(group *sync.WaitGroup) {
	defer group.Done()

	group.Add(1)

	fmt.Println("Hello")
	time.Sleep(1 * time.Second)
}

func TestWaitGroup(t *testing.T) {
	group := &sync.WaitGroup{}

	for i := 0; i < 100; i++ {
		go RunAsynchronous(group)
	}

	group.Wait()
	fmt.Println("Selesai")
} 

Coba jalankan kode program diatas, dan pastikan outputnya seperti berikut

Output kode program wait group
Output kode program wait group

Bisa dilihat jika semua goroutine selesai di jalankan barulah program nya selesai.

sync.Once

Once adalah fitur digolang yang bisa kita gunakan untuk memastikan bahwa sebuah function hanya dieksekusi hanya sekali. Jadi berapa banyak pun goroutine yang mengakses, bisa dipastikan bahwa goroutine yang pertama yang bisa mengakses function nya, goroutine yang lain akan dihiraukan, artinya function tidak akan dieksekusi lagi.

Kode : Once

Kode program once
Kode program once

Jadi contohnya kita memiliki variable counter dan kita juga punya function OnlyOnce yang digunakan untuk menaikkan atau increment variable counter. Lalu pada unit test TestOnce kita membuat once dan group yang digunakan untuk menunggu semua goroutine selesai, kemudian kita akan me running 100 goroutine yang di dalamnya kita akan memanggil function OnlyOnce menggunakan method Once dengan cara once.Do(namaFunctionYangDijalankan), terakhir kita akan melakukan print pada variable counter.

package belajar_golang_goroutines

import (
	"fmt"
	"sync"
	"testing"
)

var counter = 0

func OnlyOnce() {
	counter++
}

func TestOnce(t *testing.T) {
	once := sync.Once{}
	group := sync.WaitGroup{}

	for i := 0; i < 100; i++ {
		go func() {
			group.Add(1)
			once.Do(OnlyOnce)
			group.Done()
		}()
	}

	group.Wait()
	fmt.Println("Counter :", counter)
}

Coba jalankan kode program diatas dan pastikan jika function OnlyOnce hanya dijalankan sekali maka harusnya nilai counter adalah 1.

Output kode program once
Output kode program once

Dapat dilihat jika sesuai ekspektasi kita jika nilai counter adalah 1 artinya function OnlyOnce hanya dieksekusi sebanyak 1 kali saja.

sync.Pool

Pool adalah implementasi design pattern bernama object pool pattern, sederhanya nya Pool ini digunakan untuk menyimpan data, selanjutnya untuk menggunakan datanya kita bisa mengambil data dari Pool, dan setelah selesai menggunakan datanya, kita bisa menyimpan kembali ke Pool nya. Implementasi Pool di golang sudah aman dari problem race condition.

Kode : Membuat Pool

Kode program membuat pool
Kode program membuat pool

Pada contoh diatsa kita membuat Pool pada baris ke 10 dan kita memasukan dua data di baris ke 13 & 14. Selanjutnya seperti biasa kita akan membuat 10 goroutine dan di tiap-tiap goroutine nya kita akan mengambil data dari Pool dan menyimpannya ke variable data di baris 21, selanjutnya pada baris 22 kita akan melakukan print data ke terminal, dan terakhir kita mengembalikan data nya lagi ke Pool.

package belajar_golang_goroutines

import (
	"fmt"
	"sync"
	"testing"
)

func TestPool(t *testing.T) {
	pool := sync.Pool{}
	group := sync.WaitGroup{}

	pool.Put("Rendy")
	pool.Put("Wijaya")

	for i := 0; i < 10; i++ {
		group.Add(1)
		go func() {
			defer group.Done()

			data := pool.Get()
			fmt.Println(data)
			pool.Put(data)
		}()
	}

	group.Wait()
	fmt.Println("Selesai")
}

Coba jalankan kode program diatas untuk melihat outputnya.

Output kode program pool (1)
Output kode program pool (1)

Lihat output yang kita dapat adalah "Rendy" atau data pertama saja, hal ini terjadi karena eksekusi goroutine sangat cepat sehingga sesaat setelah diambil data dengan cepat langsung di kembalikan ke pool kembali, untuk mensimulasikan jika misal data yang diambil proses pengembalian nya lama maka kita bisa menggunakan Sleep selama satu detik sebelum data dikembalikan.

package belajar_golang_goroutines

import (
	"fmt"
	"sync"
	"testing"
	"time"
)

func TestPool(t *testing.T) {
	pool := sync.Pool{}
	group := sync.WaitGroup{}

	pool.Put("Rendy")
	pool.Put("Wijaya")

	for i := 0; i < 10; i++ {
		group.Add(1)
		go func() {
			defer group.Done()

			data := pool.Get()
			fmt.Println(data)
			time.Sleep(1 * time.Second)
			pool.Put(data)
		}()
	}

	group.Wait()
	fmt.Println("Selesai")
}

Setelah ditambahkan Sleep selama 1 detik coba jalankan kembali programnya

Output kode program pool (2)
Output kode program pool (2)

Nah kali ini output ke 3 – 10 tidak mendapat data karena data masih dipakai dan belum dikembalikan, oleh karena itu output 3 – 10 datanya menjadi nil. Selanjutnya bagaimana jika kita membutuhkan nilai default saat data sedang digunakan goroutine lain, maka kita bisa menggunakan kata kunci New seperti berikut.

Membuat default data pool
Membuat default data pool

sync.Map

Golang memiliki sebuah struct bernama sync.Map, Map ini mirip dengan golang Map yang sudah kita coba sebelumnya, namun yang membedakan Map ini aman untuk menggunakan concurrent menggunakan goroutine.

FunctionKegunaan
Store(key, value)Untuk menyimpan data ke map
Load(key)Untuk mengambil data dari map menggunakan key
Delete(key)Untuk menghapus data di map menggunakan key
Range(function(key, value))Digunakan untuk melakukan iterasi seluruh data di map
Beberapa function di map

Kode : Menggunakan Map

Kode program map
Kode program map

Disini kita membuat function AddToMap yang digunakan untuk menambahkan data ke map nantinya, kemudian kita juga membuat unit test TestMap yang di dalamnya kita membuat variable data sebagai map di baris 16, kemudian kita juga membuat 100 goroutine di bars 19 yang akan memanggil function AddToMap dan menambahkan value i terakhir kita melakukan iterasi semua data dan menampilkannya ke terminal di baris 26.

Output kode program map
Output kode program map

Bisa dilihat jika kita sudah menambahkan data ke map dan melakukan iterasinya tanpa terkendala race condition.

sync.Cond

Cond adalah implementasi locking berbasis kondisi, cond membutuhkan locker(Mutex atau RWMutex) untuk implementasi locking nya, namun berbeda dengan locker biasanya, di cond terdapat function Wait() untuk menunggu apakah perlu menunggu atau tidak. Jadi nanti saat setelah kita melakukan locking di dalam condition ini kita akan memanggil Wait(), jadi nanti jika menurut condition tersebut kita harus menunggu maka kita akan menunggu.

Setelah menggunakan Wait(), kita bisa menggunakan function Signal() untuk memberitahu sebuah goroutine agar tidak perlu menunggu lagi, sedangkan function Broadcast() digunakan untuk memberitahu semua goroutine untuk tidak perlu menunggu lagi. Untuk membuat cond kita bisa menggunakan sync.NewCond(Locker).

Kode : Menggunakan cond

Kode program cond
Kode program cond

Pada kode program diatas, hampir sama dengan locker biasanya, kita melakukan lock dan unlock pada baris kode yang ingin di lock, bedanya setelah lock kita menetukan apakah harus menunggu atau tidak menggunakan method Wait().

Setelah dipastikan harus menunggu maka eksekusi kode nya akan terhenti sementara sampai kita mengirimkan signal seperti pada baris 33 menggunakan kode cond Signal() dan jika kita ingin memberitahu semua goroutine jika tidak perlu menunggu lagi maka kita bisa menggunakan cond.Broadcast().

Atomic

Golang memiliki package yang bernama sync/atomic. Atomic merupakan package yang digunakan untuk menggunakan data primitive secara aman pada proseas concurrent tanpa khawatir terkena race condition. Contohnya sebelumnya kita telah menggunakan Mutex untuk melakukan locking ketika ingin menaikkan angka di counter, hal ini sebenarnya bisa dilakukan menggunakan Atomic package. Ada banyak sekali function pada package ini, untuk lebih detailnya bisa di review di https://pkg.go.dev/sync/atomic

Kode : Menggunaka atomic

Kode program atomic
Kode program atomic

Kode program diatas sama seperti yang sebelumnya kita buat menggunakan Mutex, bedanya kali ini kita tidak melakukan lock tetapi menggunakan package atomic, perhatikan pada baris ke 19 dimana kita melakukan increment pada variable counter dengan lebih mudah dan simple.

package belajar_golang_goroutines

import (
	"fmt"
	"sync"
	"sync/atomic"
	"testing"
)

func TestAtomic(t *testing.T) {
	var group sync.WaitGroup
	var counter int64 = 0

	for i := 0; i < 100; i++ {
		group.Add(1)
		go func() {
			defer group.Done()
			for j := 0; j < 100; j++ {
				atomic.AddInt64(&counter, 1)
			}
		}()
	}

	group.Wait()
	fmt.Println("Counter :", counter)
}

Coba jalankan kode program diatas, dan perhatikan outputnya

Output kode program atomic
Output kode program atomic

Bisa dilihat jika kita menggunakan package atomic, nilai Counter nya juga sudah benar 100.000, jadi jika kita akan melakukan manipulasi data primitive seperti number yang mana data nya diakses banyak goroutine maka kita bisa menggunakan atomic.

Penutup

Pada artikel kali ini kita telah belajar tentang race condition & package sync pada bahasa pemrogaman go. Dan pada artikel selanjutnya saya akan membahas time pada bahasa pemrogaman go.

Leave a reply:

Your email address will not be published.

Site Footer

Sliding Sidebar

About Me

About Me

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam.

Social Profiles

Facebook