Go Concurrency Power-Up: Harnessing the Semaphore Pattern for Efficient Resource Management

Sammi Aldhi Yanto
6 min readJul 31, 2023

--

Photo by Markus Spiske on Unsplash

Semaphore itu apa?

Dalam concurrency programming, Go telah menjadi primadona berkat dukungannya yang begitu memikat untuk goroutine dan channel, memungkinkan developer untuk mengeksekusi tugas secara konkuren dengan mudah. Namun, ketika tugas-tugas tersebut mengakses shared resource, perlu adanya pendekatan yang cermat untuk mengelola akses secara efisien. Di sinilah pola Semaphore hadir sebagai primitif sinkronisasi yang efisien untuk membatasi akses konkuren ke shared resource.

Misalnya, Service X akan melakukan 10k permintaan HTTP POST secara paralel ke REST API dari layanan-layanan lain, let’s say Service B. Nah, hal ini akan membuat I/O jaringan kita sibuk menangani 10k permintaan secara bersamaan dan dapat menyebabkan penurunan performa I/O. Itulah kekuatan Semaphore, kita dapat membagi 10k permintaan konkuren, misal menjadi 5 x 2k permintaan konkuren.

Nah, di sinilah Semaphore berperan. Kata Wikipedia,

Semaphore adalah variabel atau tipe data abstrak yang digunakan untuk mengontrol akses ke sumber daya bersama (shared resource) oleh beberapa proses dalam sistem konkuren seperti sistem operasi multitasking

Dalam kasus kita, daripada melakukan 10k permintaan HTTP secara bersamaan, kita dapat mengurangi jumlah proses konkuren menjadi 2k dan mengulangi proses tersebut sebanyak 5 kali. Meskipun proses ini akan memakan lebih banyak waktu dibandingkan dengan melakukan 10k permintaan secara bersamaan, namun ini memberikan waktu istirahat bagi jaringan I/O Anda karena jaringan hanya perlu menangani 2k permintaan secara bersamaan.

Analogi Berenang di Kolam Renang

Photo by Thomas Park on Unsplash

Mari kita gunakan analogi berenang di kolam renang untuk memahami konsep Semaphore. Bayangkan sebuah kolam renang yang indah pada hari musim panas yang cerah, di mana sekelompok orang ingin berenang bersama. Namun, manajemen kolam renang menetapkan aturan yang bijaksana bahwa hanya sejumlah tertentu orang yang diizinkan berada di kolam secara bersamaan untuk menjaga keselamatan dan kenyamanan. Misalnya, kolam renang tersebut hanya dapat menampung hingga 5 orang berenang sekaligus (bobot semaphore).

Kita bisa menganggap bobot Semaphore sebagai jumlah topi renang yang diperlukan oleh setiap perenang. Sebelum melompat ke kolam, setiap perenang harus memakai topi renang sebagai tanda bahwa mereka ingin berenang. Ketika kolam mencapai kapasitas maksimum (5 orang), perenang lain harus menunggu hingga ada topi renang yang tersedia. Namun begitu ada seseorang yang selesai berenang dan meletakkan kembali topi renangnya, perenang lain dapat segera mengambil topi tersebut dan bergabung dengan kegembiraan di kolam.

Dalam analogi ini, topi renang berfungsi sebagai sinyal untuk mengakses kolam, mirip dengan Semaphore yang berfungsi sebagai pengontrol akses concurrent dalam pemrograman. Semaphore memastikan bahwa hanya sejumlah tertentu goroutine yang dapat mengakses sumber daya bersama secara bersamaan, menghindari situasi berdesakan yang dapat menyebabkan deadlock atau masalah lainnya. Dengan Semaphore, kolam renang (sumber daya bersama) diatur dengan bijaksana sehingga para perenang (goroutine) dapat menikmati berenang dengan aman, nyaman, dan harmonis di bawah sinar matahari yang cerah.

Implementasi Sempahore di Go

Mari kita terjemahkan analogi tadi ke dalam bahasa Golang dengan mengimplementasikan pola Sempahore menggunakan paket sync. Oh iya, golang sendiri telah menyediakan paket sync yang dapat digunakan untuk mengimplementasikan pola Sempahore. source code nya bisa temen-temen lihat pada link berikut https://cs.opensource.google/go/x/sync/+/refs/tags/v0.3.0:semaphore/semaphore.go dan untuk instalasi packagenya temen-temen bisa nge-refers ke sini golang.org/x/sync/semaphore

package main

import (
"context"
"fmt"
"time"

"golang.org/x/sync/semaphore"
)

// Kolam renang hanya dapat menampung hingga 5 orang berenang bersamaan.
var sem = semaphore.NewWeighted(5)

// Fungsi untuk mensimulasikan berenang di kolam renang.
func berenang(pengunjung int) {
// Memperoleh izin dari Sempahore sebelum berenang.
if err := sem.Acquire(context.Background(), 1); err != nil {
fmt.Printf("Pengunjung %d: Menunggu izin untuk berenang.\n", pengunjung)
return
}

// Proses berenang.
fmt.Printf("Pengunjung %d: Menikmati berenang di kolam renang.\n", pengunjung)
time.Sleep(2 * time.Second) // Simulasi waktu berenang.

// Melepaskan izin setelah selesai berenang.
sem.Release(1)
fmt.Printf("Pengunjung %d: Keluar dari kolam renang.\n", pengunjung)
}

func main() {
// Sejumlah pengunjung ingin berenang di kolam renang.
for i := 1; i <= 10; i++ {
go berenang(i)
}

// Tunggu sejenak untuk melihat para pengunjung berenang.
time.Sleep(10 * time.Second)
}

Pada implementasi Golang di atas, kita menggunakan Sempahore sem dengan kapasitas (bobot) 5 untuk membatasi jumlah perenang (goroutine) yang dapat berenang bersamaan di kolam. Setiap perenang yang ingin berenang harus melepaskan topi dengan menggunakan sem.Acquire(). Jika jumlah perenang sudah mencapai batas kapasitas (5), perenang tambahan akan menunggu (diblokir) hingga ada slot (topi renang) yang tersedia.

Pada fungsi main(), kita menggunakan goroutine untuk mensimulasikan para perenang berenang di kolam renang. Sempahore digunakan untuk mengatur akses konkuren ke kolam sehingga tidak melebihi kapasitas yang telah ditetapkan.

Bikin implementasi semaphore sederhana menggunakan Channel

Selain menggunakan paket sync, kita juga bisa membuat Sempahore sendiri menggunakan channel. Berikut adalah contoh implementasi Sempahore menggunakan channel.

package main

import (
"fmt"
"time"
)

type Semaphore struct {
ch chan struct{}
}

// NewSemaphore membuat semafora baru dengan kapasitas yang ditentukan.
func NewSemaphore(capacity int) *Semaphore {
return &Semaphore{ch: make(chan struct{}, capacity)}
}

// Acquire mengakuisisi semafora. Jika kapasitas penuh, akan memblokir hingga ada slot kosong.
func (s *Semaphore) Acquire() {
s.ch <- struct{}{}
}

// Release melepaskan semafora. Jika tidak ada goroutine yang menunggu, itu tidak akan memblokir.
func (s *Semaphore) Release() {
select {
case <-s.ch:
default:
// Jika tidak ada goroutine yang menunggu, abaikan aksi pembebasan.
}
}

func main() {
sem := NewSemaphore(3)
doneC := make(chan bool, 1)
totProcess := 10
for i := 1; i <= totProcess; i++ {
sem.Acquire()
go func(v int) {
defer sem.Release()
longRunningProcess(v)
if v == totProcess {
doneC <- true
}
}(i)
}
<-doneC
}

func longRunningProcess(taskID int) {
fmt.Println(time.Now().Format("15:04:05"), "Running task with ID", taskID)
time.Sleep(2 * time.Second)
}

Studi kasus memanggil API dengan batasan concurrent

Photo by Aaron Burden on Unsplash

Sekarang kita akan mencoba menerapkan Sempahore untuk membatasi jumlah goroutine yang dapat memanggil API secara bersamaan. Kita akan menggunakan API https://jsonplaceholder.typicode.com/ untuk mendapatkan data todo. Kita akan membatasi jumlah goroutine yang dapat memanggil API secara bersamaan hingga 5 goroutine.

package main

import (
"context"
"fmt"
"golang.org/x/sync/semaphore"
"io"
"net/http"
"sync"
"time"
)

func main() {
// Simulasi 1000 request konkuren
numRequests := 1000

// Weight untuk semaphore
weight := 5

// Membuat semaphore dengan kapasitas weight
sem := semaphore.NewWeighted(int64(weight))

// WaitGroup untuk menunggu selesai semua goroutine
wg := sync.WaitGroup{}

// Loop untuk membuat 1000 goroutine yang melakukan request
for i := 0; i < numRequests; i++ {
wg.Add(1)
go func() {
// Melakukan akuisisi semaphore
sem.Acquire(context.Background(), 1)

// Melakukan request HTTP ke layanan
performRequest()

// Melepaskan semaphore setelah selesai request
sem.Release(1)

wg.Done()
}()
}

// Menunggu semua goroutine selesai
wg.Wait()
fmt.Println("Selesai semua request")
}

func performRequest() {
// Ganti URL sesuai dengan URL layanan yang ingin di-request
url := "https://jsonplaceholder.typicode.com/todos/1"

// Melakukan request HTTP GET ke layanan
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error saat melakukan request: %v\n", err)
return
}
defer resp.Body.Close()

// Membaca response body
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error saat membaca response body: %v\n", err)
return
}

fmt.Println(string(body))

time.Sleep(1 * time.Second) // Simulasi penggunaan sumber daya
}

Manfaat Penggunaan Sempahore

Efisiensi Sumber Daya: Dengan Sempahore, kita dapat mengelola sumber daya bersama secara efisien, mencegah pembebanan berlebihan dan potensi deadlock.
Pengendalian Konkurensi: Sempahore membantu mencegah kondisi balapan dan kerusakan data dengan membatasi akses konkuren ke sumber daya bersama.
Resiliensi Aplikasi: Dengan menggunakan Sempahore, kita dapat meningkatkan resiliensi aplikasi dengan mengatur akses konkuren ke sumber daya, mencegah kegagalan sistem yang berlebihan.

Penutup

Terimakasih buat yang udah membaca sampai akhiir, semoga bermanfaat ya :)

--

--