Go Series: Mutex & RWMutex

Dalam pengembangan aplikasi dengan Go, masalah data race sering kali muncul ketika beberapa goroutine mengakses data bersama secara bersamaan. Data race terjadi saat dua atau lebih goroutine mengakses lokasi memori yang sama, dengan setidaknya salah satunya melakukan operasi tulis, sehingga menyebabkan data tidak konsisten dan bug yang sulit dilacak.

Dengan semakin umumnya prosesor multi-core pada berbagai perangkat, dari server hingga ponsel, penguasaan alat sinkronisasi seperti Mutex dan RWMutex menjadi penting untuk memastikan aplikasi Go berjalan dengan andal. Artikel ini menjelaskan cara kerja kedua mekanisme tersebut dalam mencegah data race pada aplikasi konkuren.

Mengapa Membutuhkan Mutex di Golang?

Data race terjadi ketika beberapa goroutine mengakses dan mengubah data bersama tanpa koordinasi yang memadai. Akibatnya, nilai data bisa menjadi tidak konsisten, terutama dalam sistem kritis seperti aplikasi perbankan atau pembayaran, yang dapat menyebabkan kerugian signifikan.

Go menyediakan Mutex (Mutual Exclusion) melalui paket sync untuk mengatasi masalah ini. Mutex memastikan hanya satu goroutine yang dapat mengakses data tertentu pada satu waktu, mencegah akses konkuren yang tidak diinginkan.

Berikut adalah contoh kode yang rentan terhadap data race:

type Account struct {
    balance int
    Name    string
}

func (a *Account) Withdraw(amount int) {
    a.balance -= amount
}

func (a *Account) Deposit(amount int) {
    a.balance += amount
}

func (a *Account) GetBalance() int {
    return a.balance
}

Jika beberapa goroutine memanggil metode Deposit atau Withdraw secara bersamaan, seperti pada kode berikut, data race akan terjadi:

var account Account
account.Name = "Akun Uji"

for i := 0; i < 20; i++ {
    wg.Add(1)
    go account.Deposit(100)
}

for i := 0; i < 10; i++ {
    wg.Add(1)
    go account.Withdraw(100)
}

wg.Wait()
fmt.Printf("Saldo: %d\n", account.GetBalance())

Eksekusi kode di atas dapat menghasilkan saldo yang berbeda-beda setiap kali dijalankan karena akses konkuren yang tidak terkoordinasi. Untuk mengatasinya, kita dapat menggunakan sync.Mutex dengan menambahkannya sebagai field pada struktur Account:

type Account struct {
    balance int
    Name    string
    lock    sync.Mutex
}

func (a *Account) Withdraw(amount int, wg *sync.WaitGroup) {
    defer wg.Done()
    a.lock.Lock()
    a.balance -= amount
    a.lock.Unlock()
}

func (a *Account) Deposit(amount int, wg *sync.WaitGroup) {
    defer wg.Done()
    a.lock.Lock()
    a.balance += amount
    a.lock.Unlock()
}

func (a *Account) GetBalance() int {
    a.lock.Lock()
    defer a.lock.Unlock()
    return a.balance
}

Kode kliennya menjadi:

var account Account
var wg sync.WaitGroup

account.Name = "Akun Uji"

for i := 0; i < 20; i++ {
    wg.Add(1)
    go account.Deposit(100, &wg)
}

for i := 0; i < 10; i++ {
    wg.Add(1)
    go account.Withdraw(100, &wg)
}

wg.Wait()
fmt.Printf("Saldo: %d\n", account.GetBalance())

Dengan Mutex, akses ke balance dikunci selama operasi tulis atau baca, sehingga hasilnya konsisten.

Kapan Menggunakan RWMutex?

Meskipun Mutex efektif, penguncian untuk operasi baca dapat mengurangi efisiensi, terutama jika banyak goroutine hanya perlu membaca data tanpa mengubahnya. Untuk kasus ini, Go menyediakan RWMutex (Read-Write Mutex), yang mendukung penguncian baca bersama (shared read lock) dan penguncian tulis eksklusif (exclusive write lock).

Dengan RWMutex, beberapa goroutine dapat membaca data secara bersamaan menggunakan RLock dan RUnlock, sementara operasi tulis tetap menggunakan Lock dan Unlock. Berikut adalah implementasinya:

type Account struct {
    balance int
    Name    string
    lock    sync.RWMutex
}

func (a *Account) Withdraw(amount int, wg *sync.WaitGroup) {
    defer wg.Done()
    a.lock.Lock()
    a.balance -= amount
    a.lock.Unlock()
}

func (a *Account) Deposit(amount int, wg *sync.WaitGroup) {
    defer wg.Done()
    a.lock.Lock()
    a.balance += amount
    a.lock.Unlock()
}

func (a *Account) GetBalance() int {
    a.lock.RLock()
    defer a.lock.RUnlock()
    return a.balance
}

Dengan RWMutex, metode GetBalance hanya menggunakan penguncian baca, memungkinkan banyak goroutine mengakses balance secara bersamaan tanpa memblokir satu sama lain, selama tidak ada operasi tulis yang sedang berlangsung.

Kesimpulan

Data race adalah masalah serius dalam pemrograman konkuren yang dapat menyebabkan bug yang sulit dideteksi. Menggunakan sync.Mutex adalah cara sederhana untuk melindungi data kritis dengan memastikan akses eksklusif. Namun, jika aplikasi Anda melibatkan banyak operasi baca dengan sedikit operasi tulis, sync.RWMutex dapat meningkatkan efisiensi dengan memungkinkan akses baca bersamaan.

Penting untuk memilih alat sinkronisasi yang sesuai dengan kebutuhan aplikasi. Jika jumlah pembaca konkuren sangat tinggi, pertimbangkan pendekatan lain seperti kanal atau pola copy-on-write untuk mengoptimalkan kinerja. Dengan penggunaan Mutex dan RWMutex yang tepat, Anda dapat membangun aplikasi Go yang aman dari data race dan berjalan dengan stabil.

Last updated