Eşzamanlı programlamada, paylaşılan kaynaklara eşzamanlı erişim, data race (veri yarışı) gibi her türlü soruna yol açabilir. Beklenmedik davranışları ortadan kaldırmak için, paylaşılan kaynağa eşzamanlı erişim korunmalıdır. Bu korunan bölüme critical section (kritik bölüm) denir. Kritik bölüm, aynı anda birden fazla işlemle yürütülmemelidir.


Bu örnekte, bir banka hesabının durumunun bütünlüğünün tehlikeye atılmamasını sağlayacağız. Bunu semafor ve muteks olmak üzere iki genel senkronizasyon mekanizması kullanarak yapacağız. Bu mekanizmaların her ikisi de kritik bölümü korumak için kullanılır. Muteksleri uygularlar, çoklu kaynaklara erişimi sınırlarlar, okuyucu-yazıcı vs. gibi sorununu çözerler.



Semaforun paylaşılan bir kaynağa çoklu erişime izin verebileceği göz önüne alındığında, bunu 1 ile sınırlayacağız. Buna binary semaphore denir ve tam anlamıyla 1 kapasiteli bir tamponlu kanal kullanmakla aynıdır.


Bank iskeleti


Bu banka hesabımızın çıplak halidir. Farklı örnekler kullandığımızda güncelleyeceğiz.


package bank

type Account struct {
fund float64
}

func (a *Account) Balance() float64 {
return a.fund
}

func (a *Account) Credit(amount float64) {
a.fund += amount
}

func (a *Account) Withdraw(amount float64) {
a.fund -= amount
}

Bu, testleri çalıştırmak için kullanacağımız ana kod parçasıdır ve olduğu gibi kalacaktır.


package main

import (
"time"

"internal/bank"
)

func main() {
account := &bank.Account{}

account.Balance("-")

for k, v := range map[int]string{10: "A", 20: "B", 30: "C"} {
go account.Balance(v)
go account.Credit(float64(k), v)
go account.Withdraw(float64(k), v)
}

time.Sleep(1 * time.Second)
account.Balance("+")
}

Aynı anda bir okuma ve bir yazma


Bir seferde sadece bir goroutinin fonlara erişmesini sağlayacak. Her goroutinin fonlara özel bir erişimi olacaktır, bu yüzden biri okuyorsa diğeri bekler veya tersi olur. Bu "tam güvenli" (thread safe) bir işlem olarak değerlendirilir.


Yöntem 1: İkili semafor/tamponlu kanallar


Burada doğrudan bir semafor uygulamıyoruz, ancak bir semafor paketi oluşturmak ve kullanmak istiyorsanız, en alttaki örneğe bakın.


package bank

import (
"fmt"
"time"
)

var semaphore = make(chan struct{}, 1)

type Account struct {
fund float64
}

func (a *Account) Balance(who string) float64 {
semaphore <- struct{}{}
fmt.Println(who, "Bal", a.fund, "@", time.Now().UTC())
<-semaphore

return a.fund
}

func (a *Account) Credit(amount float64, who string) {
semaphore <- struct{}{}
fmt.Println(who, "Cre", amount, "@", time.Now().UTC())
a.fund += amount
<-semaphore
}

func (a *Account) Withdraw(amount float64, who string) {
semaphore <- struct{}{}
fmt.Println(who, "Wit", amount, "@", time.Now().UTC())
a.fund -= amount
<-semaphore
}

- Bal  0 @ 2020-04-13 14:01:33.650511 +0000 UTC
A Bal 0 @ 2020-04-13 14:01:33.650590 +0000 UTC
B Bal 0 @ 2020-04-13 14:01:33.650662 +0000 UTC
A Cre 10 @ 2020-04-13 14:01:33.650686 +0000 UTC +10
A Wit 10 @ 2020-04-13 14:01:33.650717 +0000 UTC 0
B Cre 20 @ 2020-04-13 14:01:33.650741 +0000 UTC +20
B Wit 20 @ 2020-04-13 14:01:33.650767 +0000 UTC 0
C Bal 0 @ 2020-04-13 14:01:33.650781 +0000 UTC
C Cre 30 @ 2020-04-13 14:01:33.650791 +0000 UTC +30
C Wit 30 @ 2020-04-13 14:01:33.650799 +0000 UTC 0
+ Bal 0 @ 2020-04-13 14:01:34.654512 +0000 UTC

- Bal 0 @ 2020-04-13 14:02:19.709515 +0000 UTC
B Bal 0 @ 2020-04-13 14:02:19.709617 +0000 UTC
C Cre 30 @ 2020-04-13 14:02:19.709702 +0000 UTC +30
B Cre 20 @ 2020-04-13 14:02:19.709728 +0000 UTC +50
B Wit 20 @ 2020-04-13 14:02:19.709746 +0000 UTC +30
C Bal 30 @ 2020-04-13 14:02:19.709786 +0000 UTC
A Bal 30 @ 2020-04-13 14:02:19.709801 +0000 UTC
C Wit 30 @ 2020-04-13 14:02:19.709815 +0000 UTC 0
A Cre 10 @ 2020-04-13 14:02:19.709833 +0000 UTC +10
A Wit 10 @ 2020-04-13 14:02:19.709847 +0000 UTC 0
+ Bal 0 @ 2020-04-13 14:02:20.711135 +0000 UTC

Yöntem 2: Sync.Mutex


package bank

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

type Account struct {
fund float64
mux sync.Mutex
}

func (a *Account) Balance(who string) float64 {
a.mux.Lock()
defer a.mux.Unlock()

fmt.Println(who, "Bal", a.fund, "@", time.Now().UTC())

return a.fund
}

func (a *Account) Credit(amount float64, who string) {
a.mux.Lock()
defer a.mux.Unlock()

fmt.Println(who, "Cre", amount, "@", time.Now().UTC())
a.fund += amount
}

func (a *Account) Withdraw(amount float64, who string) {
a.mux.Lock()
defer a.mux.Unlock()

fmt.Println(who, "Wit", amount, "@", time.Now().UTC())
a.fund -= amount
}

- Bal  0 @ 2020-04-13 13:47:32.016951 +0000 UTC
A Bal 0 @ 2020-04-13 13:47:32.017039 +0000 UTC
B Bal 0 @ 2020-04-13 13:47:32.017177 +0000 UTC
A Cre 10 @ 2020-04-13 13:47:32.017188 +0000 UTC +10
A Wit 10 @ 2020-04-13 13:47:32.017216 +0000 UTC 0
C Bal 0 @ 2020-04-13 13:47:32.017231 +0000 UTC
C Cre 30 @ 2020-04-13 13:47:32.017244 +0000 UTC +30
B Cre 20 @ 2020-04-13 13:47:32.017254 +0000 UTC +50
B Wit 20 @ 2020-04-13 13:47:32.017266 +0000 UTC +30
C Wit 30 @ 2020-04-13 13:47:32.017280 +0000 UTC 0
+ Bal 0 @ 2020-04-13 13:47:33.021482 +0000 UTC

- Bal 0 @ 2020-04-13 13:48:45.885885 +0000 UTC
A Bal 0 @ 2020-04-13 13:48:45.885986 +0000 UTC
B Cre 20 @ 2020-04-13 13:48:45.886052 +0000 UTC +20
A Wit 10 @ 2020-04-13 13:48:45.886085 +0000 UTC +10
B Bal 10 @ 2020-04-13 13:48:45.886114 +0000 UTC
C Bal 10 @ 2020-04-13 13:48:45.886130 +0000 UTC
B Wit 20 @ 2020-04-13 13:48:45.886142 +0000 UTC -10
C Cre 30 @ 2020-04-13 13:48:45.886157 +0000 UTC +20
C Wit 30 @ 2020-04-13 13:48:45.886169 +0000 UTC -10
A Cre 10 @ 2020-04-13 13:48:45.886183 +0000 UTC 0
+ Bal 0 @ 2020-04-13 13:48:46.891075 +0000 UTC

Aynı anda çok okuma ve bir yazma


Birçok goroutinin aynı anda fonlara (paylaşılan bir değişken/kaynak) erişmesine izin verme. Ancak, sadece bir kişi yazabilir ve bu olduğunda, okumalar da engellenir. Hiç kimse yazmıyorsa, "şartlı güvenli" (conditionally safe) bir işlem olarak değerlendirildiğinden birçok okumaya izin verilir. Tekrarlıyorum, kimse aynı anda yazmadığı sürece! Bu, yüksek oranda okuma gerektiren durumlar için iyidir.


package bank

import "sync"

type Account struct {
fund float64
mux sync.RWMutex
}

func (a *Account) Balance() float64 {
a.mux.RLock()
defer a.mux.RUnlock()

return a.fund
}

func (a *Account) Credit(amount float64) {
a.mux.Lock()
defer a.mux.Unlock()

a.fund += amount
}

func (a *Account) Withdraw(amount float64) {
a.mux.Lock()
defer a.mux.Unlock()

a.fund -= amount
}

- Bal  0 @ 2020-04-13 13:53:44.060959 +0000 UTC
A Bal 0 @ 2020-04-13 13:53:44.061042 +0000 UTC
B Cre 20 @ 2020-04-13 13:53:44.061125 +0000 UTC +20
B Bal 20 @ 2020-04-13 13:53:44.061158 +0000 UTC
C Bal 20 @ 2020-04-13 13:53:44.061161 +0000 UTC
A Cre 10 @ 2020-04-13 13:53:44.061212 +0000 UTC +30
A Wit 10 @ 2020-04-13 13:53:44.061233 +0000 UTC +20
B Wit 20 @ 2020-04-13 13:53:44.061248 +0000 UTC 0
C Cre 30 @ 2020-04-13 13:53:44.061262 +0000 UTC +30
C Wit 30 @ 2020-04-13 13:53:44.061276 +0000 UTC 0
+ Bal 0 @ 2020-04-13 13:53:45.066051 +0000 UTC

- Bal 0 @ 2020-04-13 13:55:31.374249 +0000 UTC
B Bal 0 @ 2020-04-13 13:55:31.374342 +0000 UTC
A Bal 0 @ 2020-04-13 13:55:31.374336 +0000 UTC
A Cre 10 @ 2020-04-13 13:55:31.374470 +0000 UTC +10
C Bal 10 @ 2020-04-13 13:55:31.374482 +0000 UTC
B Cre 20 @ 2020-04-13 13:55:31.374515 +0000 UTC +30
B Wit 20 @ 2020-04-13 13:55:31.374543 +0000 UTC +10
A Wit 10 @ 2020-04-13 13:55:31.374563 +0000 UTC 0
C Cre 30 @ 2020-04-13 13:55:31.374577 +0000 UTC +30
C Wit 30 @ 2020-04-13 13:55:31.374590 +0000 UTC 0
+ Bal 0 @ 2020-04-13 13:55:32.378968 +0000 UTC

Semafor paketi


Bir semafor paketi buna benzer.


package semaphore

type Semaphore chan struct {}

// New returns Semaphore channel. Use 1 for "mutual exclusion".
func New(n int) Semaphore {
return make(Semaphore, n)
}

// acquire `n` amount of resource slots in semaphore channel.
func (s Semaphore) Acquire(n int) {
for i := 0; i < n; i++ {
s <- struct{}{}
}
}

// release `n` amount of resource slots in semaphore channel.
func (s Semaphore) Release(n int) {
for i := 0; i < n; i++ {
<- s
}
}

Kullanım


İsterseniz, aşağıda gösterildiği gibi banka örneğimizle kullanabilirsiniz.


package bank

import (
"internal/semaphore"
)

const slot = 1

type locker struct {
semaphore.Semaphore
}

func newLocker() locker {
return locker{semaphore.New(slot)}
}

func (l locker) lock() {
l.Semaphore.Acquire(slot)
}

func (l locker) release() {
l.Semaphore.Release(slot)
}

package bank

import (
"fmt"
"time"
)

type Account struct {
fund float64
locker locker
}

func New() *Account {
return &Account{
fund: 0,
locker: newLocker(),
}
}

func (a *Account) Balance(who string) float64 {
a.locker.lock()
fmt.Println(who, "Bal", a.fund, "@", time.Now().UTC())
a.locker.release()

return a.fund
}

func (a *Account) Credit(amount float64, who string) {
a.locker.lock()
fmt.Println(who, "Cre", amount, "@", time.Now().UTC())
a.fund += amount
a.locker.release()
}

func (a *Account) Withdraw(amount float64, who string) {
a.locker.lock()
fmt.Println(who, "Wit", amount, "@", time.Now().UTC())
a.fund -= amount
a.locker.release()
}

İsteğe bağlı olarak paketi kendi başına kullanmak istiyorsanız, burada basit bir örnek var. Bu durumda, kaynak yuvaları yukarıdaki 1 yuvalı banka örneğimizin aksine esnek bir şekilde kullanılır.


package main

import "fmt"

type (
// empty is a 0 byte struct.
empty struct {}
// semaphore is a channel with empty struct.
semaphore chan empty
)

func main() {
// Initialise semaphore channel with 10 resource slots.
s := make(semaphore, 10)

// Acquire 7 resource slots.
s.acquire(1)
s.acquire(2)
s.acquire(4)
fmt.Println("Available resource slots:", len(s))

// Release 2 resource slots.
s.release(2)
fmt.Println("Available resource slots:", len(s))
}

// acquire `n` amount of resource slots in semaphore channel.
func (s semaphore) acquire(n int) {
for i := 0; i < n; i++ {
s <- empty{}
}
}

// release `n` amount of resource slots in semaphore channel.
func (s semaphore) release(n int) {
for i := 0; i < n; i++ {
<- s
}
}

Referanslar