Paket Pemula Konkurensi Go

Ikhtisar
Pengantar Singkat
Bahasa Go menyediakan banyak alat untuk mengelola konkurensi. Dalam artikel ini, saya akan memperkenalkan beberapa di antaranya beserta trik-trik yang relevan.
Goroutine?
goroutine adalah model konkurensi baru yang didukung oleh bahasa Go. Secara umum, program meminta OS thread dari OS untuk menjalankan banyak tugas secara bersamaan, dan menjalankan tugas secara paralel sebanyak jumlah core. Untuk melakukan konkurensi dalam skala yang lebih kecil, program membuat green thread di userland, sehingga beberapa green thread bergantian menjalankan tugas di dalam satu OS thread. Namun, goroutine membuat bentuk green thread ini menjadi lebih kecil dan lebih efisien. Goroutine menggunakan memori lebih sedikit dibandingkan thread, dan dapat dibuat serta dialihkan lebih cepat daripada thread.
Untuk menggunakan goroutine, cukup dengan menggunakan kata kunci go. Hal ini memungkinkan kode sinkron dijalankan sebagai kode asinkron secara intuitif selama proses penulisan program.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch := make(chan struct{})
10 go func() {
11 defer close(ch)
12 time.Sleep(1 * time.Second)
13 fmt.Println("Hello, World!")
14 }()
15
16 fmt.Println("Waiting for goroutine...")
17 for range ch {}
18}
Kode ini mengubah kode sinkron yang beristirahat selama 1 detik lalu mencetak Hello, World! menjadi alur asinkron. Contoh ini memang sederhana, tetapi ketika kode yang lebih kompleks diubah dari sinkron menjadi asinkron, keterbacaan, visibilitas, dan pemahaman kode menjadi lebih baik dibandingkan pendekatan yang ada seperti async await atau promise.
Namun, dalam banyak kasus, goroutine yang buruk sering dihasilkan ketika seseorang tidak memahami alur yang hanya memanggil kode sinkron secara asinkron, maupun alur seperti fork & join (yang menyerupai divide and conquer). Untuk mengantisipasi kasus seperti ini, artikel ini akan memperkenalkan beberapa metode dan teknik.
Pengelolaan Konkurensi
context
Mungkin mengejutkan bahwa context muncul sebagai teknik pertama. Namun, dalam bahasa Go, context memiliki peran yang sangat baik dalam mengelola seluruh pohon pekerjaan, melampaui sekadar fungsi pembatalan. Untuk pembaca yang belum familiar, saya akan menjelaskan package ini secara singkat.
1package main
2
3func main() {
4 ctx, cancel := context.WithCancel(context.Background())
5 defer cancel()
6
7 go func() {
8 <-ctx.Done()
9 fmt.Println("Context is done!")
10 }()
11
12 time.Sleep(1 * time.Second)
13
14 cancel()
15
16 time.Sleep(1 * time.Second)
17}
Kode di atas menggunakan context untuk mencetak Context is done! setelah 1 detik. context dapat memeriksa status pembatalan melalui metode Done(), dan menyediakan berbagai cara pembatalan melalui metode seperti WithCancel, WithTimeout, WithDeadline, dan WithValue.
Mari kita buat contoh sederhana. Misalkan Anda menulis kode untuk mengambil data user, post, dan comment dengan menggunakan pola aggregator. Jika semua permintaan harus selesai dalam 2 detik, Anda dapat menulisnya sebagai berikut.
1package main
2
3func main() {
4 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
5 defer cancel()
6
7 ch := make(chan struct{})
8 go func() {
9 defer close(ch)
10 user := getUser(ctx)
11 post := getPost(ctx)
12 comment := getComment(ctx)
13
14 fmt.Println(user, post, comment)
15 }()
16
17 select {
18 case <-ctx.Done():
19 fmt.Println("Timeout!")
20 case <-ch:
21 fmt.Println("All data is fetched!")
22 }
23}
Kode di atas akan mencetak Timeout! jika tidak semua data berhasil diambil dalam 2 detik, dan mencetak All data is fetched! jika semua data berhasil diambil. Dengan cara ini, context memudahkan pengelolaan pembatalan dan timeout pada kode yang menjalankan banyak goroutine.
Berbagai fungsi dan metode terkait context dapat dilihat di godoc context. Saya harap Anda dapat mempelajari bagian yang sederhana dan menggunakannya dengan nyaman.
channel
unbuffered channel
channel adalah alat untuk komunikasi antar goroutine. channel dapat dibuat dengan make(chan T). Di sini, T adalah tipe data yang akan dikirim melalui channel tersebut. Data dapat dikirim dan diterima menggunakan <-, dan channel dapat ditutup menggunakan close.
1package main
2
3func main() {
4 ch := make(chan int)
5 go func() {
6 ch <- 1
7 ch <- 2
8 close(ch)
9 }()
10
11 for i := range ch {
12 fmt.Println(i)
13 }
14}
Kode di atas menggunakan channel untuk mencetak 1 dan 2. Contoh ini hanya menunjukkan pengiriman dan penerimaan nilai secara sederhana. Namun, channel menyediakan lebih banyak fitur daripada itu. Pertama, mari kita pelajari buffered channel dan unbuffered channel. Sebelum memulai, contoh di atas menggunakan unbuffered channel, sehingga aksi mengirim data ke channel dan aksi menerima data harus terjadi secara bersamaan. Jika kedua aksi ini tidak terjadi secara bersamaan, deadlock dapat terjadi.
buffered channel
Bagaimana jika kode di atas bukan hanya mencetak, melainkan dua proses yang menjalankan pekerjaan berat? Jika proses kedua membaca dan memproses data namun memakan waktu lama, proses pertama juga akan terhenti selama waktu tersebut. Untuk mencegah situasi ini, kita dapat menggunakan buffered channel.
1package main
2
3func main() {
4 ch := make(chan int, 2)
5 go func() {
6 ch <- 1
7 ch <- 2
8 close(ch)
9 }()
10
11 for i := range ch {
12 fmt.Println(i)
13 }
14}
Kode di atas menggunakan buffered channel untuk mencetak 1 dan 2. Pada kode ini, buffered channel digunakan sehingga aksi mengirim data ke channel dan aksi menerima data tidak harus terjadi secara bersamaan. Dengan menyediakan buffer pada channel, terdapat ruang sebanyak panjang buffer tersebut, sehingga keterlambatan pekerjaan yang disebabkan oleh pekerjaan prioritas rendah dapat dicegah.
select
Ketika menangani beberapa channel, penggunaan sintaks select memudahkan implementasi struktur fan-in.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch1 := make(chan int, 10)
10 ch2 := make(chan int, 10)
11 ch3 := make(chan int, 10)
12
13 go func() {
14 for {
15 ch1 <- 1
16 time.Sleep(1 * time.Second)
17 }
18 }()
19 go func() {
20 for {
21 ch2 <- 2
22 time.Sleep(2 * time.Second)
23 }
24 }()
25 go func() {
26 for {
27 ch3 <- 3
28 time.Sleep(3 * time.Second)
29 }
30 }()
31
32 for i := 0; i < 3; i++ {
33 select {
34 case v := <-ch1:
35 fmt.Println(v)
36 case v := <-ch2:
37 fmt.Println(v)
38 case v := <-ch3:
39 fmt.Println(v)
40 }
41 }
42}
Kode di atas membuat tiga channel yang secara periodik mengirimkan 1, 2, dan 3, lalu menggunakan select untuk menerima nilai dari channel dan mencetaknya. Dengan cara ini, select memungkinkan penerimaan data dari beberapa channel secara bersamaan dan pemrosesan setiap nilai segera setelah diterima.
for range
channel dapat menerima data dengan mudah menggunakan for range. Ketika for range digunakan pada channel, loop akan dijalankan setiap kali data ditambahkan ke channel tersebut, dan loop akan berakhir ketika channel ditutup.
1package main
2
3func main() {
4 ch := make(chan int)
5 go func() {
6 ch <- 1
7 ch <- 2
8 close(ch)
9 }()
10
11 for i := range ch {
12 fmt.Println(i)
13 }
14}
Kode di atas menggunakan channel untuk mencetak 1 dan 2. Pada kode ini, for range digunakan untuk menerima dan mencetak data setiap kali data ditambahkan ke channel. Ketika channel ditutup, loop berakhir.
Seperti yang telah ditulis beberapa kali di atas, sintaks ini juga dapat digunakan sebagai mekanisme sinkronisasi sederhana.
1package main
2
3func main() {
4 ch := make(chan struct{})
5 go func() {
6 defer close(ch)
7 time.Sleep(1 * time.Second)
8 fmt.Println("Hello, World!")
9 }()
10
11 fmt.Println("Waiting for goroutine...")
12 for range ch {}
13}
Kode di atas beristirahat selama 1 detik lalu mencetak Hello, World!. Pada kode ini, channel digunakan untuk mengubah kode sinkron menjadi kode asinkron. Dengan cara ini, penggunaan channel memudahkan konversi kode sinkron menjadi asinkron dan penetapan titik join.
etc
- Mengirim atau menerima data pada nil channel dapat menyebabkan infinite loop dan deadlock.
- Mengirim data ke channel yang sudah ditutup akan menyebabkan panic.
- Meskipun channel tidak ditutup secara eksplisit, GC akan membersihkannya dan menutup channel tersebut.
mutex
spinlock
spinlock adalah metode sinkronisasi yang terus mencoba mengunci di dalam loop. Di bahasa Go, spinlock dapat diimplementasikan dengan mudah menggunakan pointer.
1package spinlock
2
3import (
4 "runtime"
5 "sync/atomic"
6)
7
8type SpinLock struct {
9 lock uintptr
10}
11
12func (s *SpinLock) Lock() {
13 for !atomic.CompareAndSwapUintptr(&s.lock, 0, 1) {
14 runtime.Gosched()
15 }
16}
17
18func (s *SpinLock) Unlock() {
19 atomic.StoreUintptr(&s.lock, 0)
20}
21
22func NewSpinLock() *SpinLock {
23 return &SpinLock{}
24}
Kode di atas adalah implementasi package spinlock. Pada kode ini, SpinLock diimplementasikan menggunakan package sync/atomic. Metode Lock mencoba mengunci menggunakan atomic.CompareAndSwapUintptr, dan metode Unlock melepas kunci menggunakan atomic.StoreUintptr. Karena metode ini terus mencoba mengunci tanpa henti, CPU akan terus digunakan hingga kunci diperoleh, dan hal ini dapat menyebabkan infinite loop. Oleh karena itu, spinlock sebaiknya digunakan untuk sinkronisasi sederhana atau hanya dalam durasi yang singkat.
sync.Mutex
mutex adalah alat untuk sinkronisasi antar goroutine. mutex yang disediakan oleh package sync memiliki metode seperti Lock, Unlock, RLock, dan RUnlock. mutex dapat dibuat dengan sync.Mutex, dan sync.RWMutex dapat digunakan untuk kunci baca/tulis.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 var mu sync.Mutex
9 var count int
10
11 go func() {
12 mu.Lock()
13 count++
14 mu.Unlock()
15 }()
16
17 mu.Lock()
18 count++
19 mu.Unlock()
20
21 println(count)
22}
Pada kode di atas, dua goroutine hampir bersamaan mengakses variabel count yang sama. Dengan menggunakan mutex untuk membuat bagian kode yang mengakses variabel count menjadi critical section, akses konkuren ke count dapat dicegah. Dengan demikian, kode ini akan selalu mencetak 2 berapa kali pun dijalankan.
sync.RWMutex
sync.RWMutex adalah mutex yang dapat digunakan dengan membedakan kunci baca dan kunci tulis. Kunci baca dapat dipasang dan dilepas menggunakan metode RLock dan RUnlock.
1package cmap
2
3import (
4 "sync"
5)
6
7type ConcurrentMap[K comparable, V any] struct {
8 sync.RWMutex
9 data map[K]V
10}
11
12func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
13 m.RLock()
14 defer m.RUnlock()
15
16 value, ok := m.data[key]
17 return value, ok
18}
19
20func (m *ConcurrentMap[K, V]) Set(key K, value V) {
21 m.Lock()
22 defer m.Unlock()
23
24 m.data[key] = value
25}
Kode di atas adalah implementasi ConcurrentMap menggunakan sync.RWMutex. Pada kode ini, metode Get memasang kunci baca, dan metode Set memasang kunci tulis, sehingga map data dapat diakses dan dimodifikasi dengan aman. Alasan penggunaan kunci baca adalah agar ketika operasi baca cukup banyak, beberapa goroutine dapat membaca secara bersamaan dengan hanya memasang kunci baca, tanpa memasang kunci tulis. Dengan demikian, pada situasi di mana tidak ada perubahan state dan kunci tulis tidak perlu dipasang, performa dapat ditingkatkan dengan hanya memasang kunci baca.
fakelock
fakelock adalah trik sederhana yang mengimplementasikan sync.Locker. Struct ini menyediakan metode yang sama dengan sync.Mutex, tetapi tidak melakukan apa pun secara nyata.
1package fakelock
2
3type FakeLock struct{}
4
5func (f *FakeLock) Lock() {}
6
7func (f *FakeLock) Unlock() {}
Kode di atas adalah implementasi package fakelock. Package ini mengimplementasikan sync.Locker dan menyediakan metode Lock dan Unlock, tetapi sebenarnya tidak melakukan apa pun. Alasan mengapa kode seperti ini diperlukan akan saya jelaskan pada kesempatan lain.
waitgroup
sync.WaitGroup
sync.WaitGroup adalah alat untuk menunggu hingga semua pekerjaan goroutine selesai. Alat ini menyediakan metode Add, Done, dan Wait. Metode Add digunakan untuk menambah jumlah goroutine, metode Done digunakan untuk memberi tahu bahwa pekerjaan sebuah goroutine telah selesai, dan metode Wait digunakan untuk menunggu hingga semua pekerjaan goroutine selesai.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{}
10 c := atomic.Int64{}
11
12 for i := 0; i < 100 ; i++ {
13 wg.Add(1)
14 go func() {
15 defer wg.Done()
16 c.Add(1)
17 }()
18 }
19
20 wg.Wait()
21 println(c.Load())
22}
Kode di atas menggunakan sync.WaitGroup agar 100 goroutine secara bersamaan menambahkan nilai ke variabel c. Pada kode ini, sync.WaitGroup digunakan untuk menunggu hingga semua goroutine selesai, lalu nilai yang telah ditambahkan ke c dicetak. Untuk kasus fork & join dengan beberapa pekerjaan saja, penggunaan channel saja sudah cukup, tetapi untuk fork & join dengan banyak pekerjaan, sync.WaitGroup juga merupakan pilihan yang baik.
with slice
Jika digunakan bersama slice, waitgroup dapat menjadi alat yang sangat baik untuk mengelola pekerjaan yang dijalankan secara konkuren tanpa lock.
1package main
2
3import (
4 "fmt"
5 "sync"
6 "rand"
7)
8
9func main() {
10 var wg sync.WaitGroup
11 arr := [10]int{}
12
13 for i := 0; i < 10; i++ {
14 wg.Add(1)
15 go func(id int) {
16 defer wg.Done()
17
18 arr[id] = rand.Intn(100)
19 }(i)
20 }
21
22 wg.Wait()
23 fmt.Println("Done")
24
25 for i, v := range arr {
26 fmt.Printf("arr[%d] = %d\n", i, v)
27 }
28}
Kode di atas menggunakan waitgroup saja, sehingga setiap goroutine secara bersamaan menghasilkan 10 bilangan bulat acak dan menyimpannya pada indeks yang telah ditentukan. Pada kode ini, waitgroup digunakan untuk menunggu hingga semua goroutine selesai, lalu mencetak Done. Dengan cara ini, waitgroup memungkinkan beberapa goroutine bekerja secara bersamaan, menyimpan data tanpa lock hingga semua goroutine selesai, dan kemudian melakukan pasca-pemrosesan secara sekaligus setelah pekerjaan selesai.
golang.org/x/sync/errgroup.ErrGroup
errgroup adalah package yang merupakan perluasan dari sync.WaitGroup. Berbeda dengan sync.WaitGroup, errgroup akan membatalkan semua goroutine dan mengembalikan error jika terjadi error pada salah satu pekerjaan goroutine.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/errgroup"
7)
8
9func main() {
10 g, ctx := errgroup.WithContext(context.Background())
11 _ = ctx
12
13 for i := 0; i < 10; i++ {
14 i := i
15 g.Go(func() error {
16 if i == 5 {
17 return fmt.Errorf("error")
18 }
19 return nil
20 })
21 }
22
23 if err := g.Wait(); err != nil {
24 fmt.Println(err)
25 }
26}
Kode di atas menggunakan errgroup untuk membuat 10 goroutine, dengan error yang sengaja dimunculkan pada goroutine kelima. Error sengaja dibuat pada goroutine kelima untuk menunjukkan kasus terjadinya error. Namun, dalam penggunaan sesungguhnya, errgroup digunakan untuk membuat goroutine, lalu melakukan berbagai pasca-pemrosesan untuk setiap kasus error yang terjadi pada masing-masing goroutine.
once
Ini adalah alat untuk menjalankan kode yang seharusnya hanya dieksekusi satu kali. Kode terkait dapat dijalankan melalui konstruktor berikut.
1func OnceFunc(f func()) func()
2func OnceValue[T any](f func() T) func() T
3func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
OnceFunc
OnceFunc memastikan bahwa fungsi tersebut hanya dapat dijalankan tepat satu kali selama seluruh program berjalan.
1package main
2
3import "sync"
4
5func main() {
6 once := sync.OnceFunc(func() {
7 println("Hello, World!")
8 })
9
10 once()
11 once()
12 once()
13 once()
14 once()
15}
Kode di atas menggunakan sync.OnceFunc untuk mencetak Hello, World!. Pada kode ini, sync.OnceFunc digunakan untuk membuat fungsi once, dan meskipun fungsi once dipanggil berkali-kali, Hello, World! hanya dicetak satu kali.
OnceValue
OnceValue tidak hanya memastikan bahwa fungsi tersebut dijalankan tepat satu kali, tetapi juga menyimpan nilai kembalian fungsi tersebut dan mengembalikan nilai yang tersimpan ketika dipanggil kembali.
1package main
2
3import "sync"
4
5func main() {
6 c := 0
7 once := sync.OnceValue(func() int {
8 c += 1
9 return c
10 })
11
12 println(once())
13 println(once())
14 println(once())
15 println(once())
16 println(once())
17}
Kode di atas menggunakan sync.OnceValue untuk menaikkan variabel c sebesar 1. Pada kode ini, sync.OnceValue digunakan untuk membuat fungsi once, dan meskipun fungsi once dipanggil berkali-kali, variabel c hanya dinaikkan satu kali dan nilai 1 yang dikembalikan.
OnceValues
OnceValues bekerja sama dengan OnceValue, tetapi dapat mengembalikan banyak nilai.
1package main
2
3import "sync"
4
5func main() {
6 c := 0
7 once := sync.OnceValues(func() (int, int) {
8 c += 1
9 return c, c
10 })
11
12 a, b := once()
13 println(a, b)
14 a, b = once()
15 println(a, b)
16 a, b = once()
17 println(a, b)
18 a, b = once()
19 println(a, b)
20 a, b = once()
21 println(a, b)
22}
Kode di atas menggunakan sync.OnceValues untuk menaikkan variabel c sebesar 1. Pada kode ini, sync.OnceValues digunakan untuk membuat fungsi once, dan meskipun fungsi once dipanggil berkali-kali, variabel c hanya dinaikkan satu kali dan nilai 1 yang dikembalikan.
atomic
Package atomic adalah package yang menyediakan operasi atomik. Package ini menyediakan metode seperti Add, CompareAndSwap, Load, Store, dan Swap, tetapi belakangan ini penggunaan tipe seperti Int64, Uint64, dan Pointer lebih dianjurkan.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{}
10 c := atomic.Int64{}
11
12 for i := 0; i < 100 ; i++ {
13 wg.Add(1)
14 go func() {
15 defer wg.Done()
16 c.Add(1)
17 }()
18 }
19
20 wg.Wait()
21 println(c.Load())
22}
Ini adalah contoh yang digunakan sebelumnya. Kode ini menggunakan tipe atomic.Int64 untuk menaikkan variabel c secara atomik. Dengan metode Add dan Load, variabel dapat dinaikkan dan dibaca secara atomik. Selain itu, metode Store dapat digunakan untuk menyimpan nilai, metode Swap untuk menukar nilai, dan metode CompareAndSwap untuk membandingkan nilai lalu menggantinya jika sesuai.
cond
sync.Cond
Package cond adalah package yang menyediakan condition variable. Package cond dapat dibuat dengan sync.Cond, dan menyediakan metode Wait, Signal, dan Broadcast.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 c := sync.NewCond(&sync.Mutex{})
9 ready := false
10
11 go func() {
12 c.L.Lock()
13 ready = true
14 c.Signal()
15 c.L.Unlock()
16 }()
17
18 c.L.Lock()
19 for !ready {
20 c.Wait()
21 }
22 c.L.Unlock()
23
24 println("Ready!")
25}
Kode di atas menggunakan sync.Cond untuk menunggu hingga variabel ready menjadi true. Pada kode ini, sync.Cond digunakan untuk menunggu hingga variabel ready menjadi true, lalu mencetak Ready!. Dengan cara ini, sync.Cond memungkinkan beberapa goroutine menunggu secara bersamaan hingga kondisi tertentu terpenuhi.
Dengan memanfaatkan hal ini, sebuah queue sederhana dapat diimplementasikan.
1package queue
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8type Node[T any] struct {
9 Value T
10 Next *Node[T]
11}
12
13type Queue[T any] struct {
14 sync.Mutex
15 Cond *sync.Cond
16 Head *Node[T]
17 Tail *Node[T]
18 Len int
19}
20
21func New[T any]() *Queue[T] {
22 q := &Queue[T]{}
23 q.Cond = sync.NewCond(&q.Mutex)
24 return q
25}
26
27func (q *Queue[T]) Push(value T) {
28 q.Lock()
29 defer q.Unlock()
30
31 node := &Node[T]{Value: value}
32 if q.Len == 0 {
33 q.Head = node
34 q.Tail = node
35 } else {
36 q.Tail.Next = node
37 q.Tail = node
38 }
39 q.Len++
40 q.Cond.Signal()
41}
42
43func (q *Queue[T]) Pop() T {
44 q.Lock()
45 defer q.Unlock()
46
47 for q.Len == 0 {
48 q.Cond.Wait()
49 }
50
51 node := q.Head
52 q.Head = q.Head.Next
53 q.Len--
54 return node.Value
55}
Dengan menggunakan sync.Cond seperti ini, alih-alih menggunakan spin-lock yang banyak mengonsumsi CPU, program dapat menunggu secara efisien dan kembali bekerja ketika kondisi terpenuhi.
semaphore
golang.org/x/sync/semaphore.Semaphore
Package semaphore adalah package yang menyediakan semaphore. Package semaphore dapat dibuat dengan golang.org/x/sync/semaphore.Semaphore, dan menyediakan metode Acquire, Release, dan TryAcquire.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/semaphore"
7)
8
9func main() {
10 s := semaphore.NewWeighted(1)
11
12 if s.TryAcquire(1) {
13 fmt.Println("Acquired!")
14 } else {
15 fmt.Println("Not Acquired!")
16 }
17
18 s.Release(1)
19}
Kode di atas membuat semaphore menggunakan semaphore, lalu memperoleh semaphore dengan metode Acquire dan melepaskannya dengan metode Release. Pada kode ini, ditunjukkan cara memperoleh dan melepaskan semaphore menggunakan semaphore.
Penutup
Sampai di sini saja mungkin sudah cukup untuk materi dasarnya. Berdasarkan artikel ini, saya harap Anda dapat memahami cara mengelola konkurensi menggunakan goroutine dan dapat menerapkannya secara langsung. Semoga artikel ini bermanfaat bagi Anda. Terima kasih.