Concurrency
Kurgu şu şekilde:Bir meslektaşınız, URL'lerin durumunu kontrol eden CheckWebsites isminde bir fonksiyon yazar.
1
package concurrency
2
3
type WebsiteChecker func(string) bool
4
5
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
6
results := make(map[string]bool)
7
8
for _, url := range urls {
9
results[url] = wc(url)
10
}
11
12
return results
13
}
Copied!
Her bir URL'in kontrolünü boolean değer olarak tutan bir map döner - başarılı cevaplar için true, başarısız cevaplar için false.
Ayrıca, parametre olarak URL alan ve boolean değer dönen WebsiteChecker'a parametre göndermelisiniz. Bu, bütün web sitelerini kontrol etmek için kullanılacak.
Dependency Injection kulanmak, gerçek HTTP çağrıları yapmadan fonksiyonu test etmemizi sağlar, güvenilir ve hızlı hale getirir
İşte, onların yazdığı test :
1
package concurrency
2
3
import (
4
"reflect"
5
"testing"
6
)
7
8
func mockWebsiteChecker(url string) bool {
9
if url == "waat://furhurterwe.geds" {
10
return false
11
}
12
return true
13
}
14
15
func TestCheckWebsites(t *testing.T) {
16
websites := []string{
17
"http://google.com",
18
"http://blog.gypsydave5.com",
19
"waat://furhurterwe.geds",
20
}
21
22
want := map[string]bool{
23
"http://google.com": true,
24
"http://blog.gypsydave5.com": true,
25
"waat://furhurterwe.geds": false,
26
}
27
28
got := CheckWebsites(mockWebsiteChecker, websites)
29
30
if !reflect.DeepEqual(want, got) {
31
t.Fatalf("Wanted %v, got %v", want, got)
32
}
33
}
Copied!
Fonksiyon kullanımda ve yüzlerce web sitesinin kontrolünde kullanılıyor. Ancak meslektaşınız fonksiyonun yavaş oluduğuna dair şikayetler almaya başladı, bu yüzden sizden hızlandırmak için yardım istedi.

Test yaz

CheckWebsites'ın hızını test etmek için benchmark kullanalım bu sayede yaptığımız değişikliklerin etkilerini görürüz
1
package concurrency
2
3
import (
4
"testing"
5
"time"
6
)
7
8
func slowStubWebsiteChecker(_ string) bool {
9
time.Sleep(20 * time.Millisecond)
10
return true
11
}
12
13
func BenchmarkCheckWebsites(b *testing.B) {
14
urls := make([]string, 100)
15
for i := 0; i < len(urls); i++ {
16
urls[i] = "a url"
17
}
18
19
for i := 0; i < b.N; i++ {
20
CheckWebsites(slowStubWebsiteChecker, urls)
21
}
22
}
Copied!
Benchmark, yüz tane url'in bulunduğu slice'ı ve WebsiteChecker'ın sahte bir implementasyonunu kullanarak CheckWebsite'ı test eder. slotStubWebsiteChecker kasten yavaş. Tam olarak yirmi milisaniye beklemek için time.Sleep kullanır ve sonra true döner.
go test -bench=. (veya Windows Powershell'de iseniz go test -bench=".") kullanarak benchmark çalıştırdığımızda:
1
pkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v0
2
BenchmarkCheckWebsites-4 1 2249228637 ns/op
3
PASS
4
ok github.com/gypsydave5/learn-go-with-tests/concurrency/v0 2.268s
Copied!
CheckWebsites, 2249228637 nanosaniye olarak ölçüldü - yaklaşık iki saniye.
Hadi bunu daha hızlı yapmayı deneyelim.

Testi geçecek kadar kod yaz

Sonunda concurrency, bir kerede birden fazla işlem, hakkında konuşabiliriz. Bu her gün doğal olarak yaptığımız bir şey.
Örneğin, bu sabah bir fincan çay yaptım. Su ısıtıcısını açtım ve kaynamasını beklerken, sütü buzdolabından çıkardım, çayı dolaptan çıkardım, favori bardağımı buldum, çay poşetini bardağa koydum, su ısıtıcısı kaynadığında, suyu bardağa koydum.
Yapmadığım şey ise, su ısıtıcısını açmak ve boş boş orada beklemekti, ardında su ısıtıcıs kaynadıktan sonra diğer her şeyi yapmaktı
Eğer ilk yöntem ile çay yapmak neden daha hızlı olduğunu anlarsanız, CheckWebsites'ı nasıl daha hızlı yapacağımızı anlayabilirsiniz. Sıradaki web sitesine istek atmadan önce web sitesinden cevap beklemek yerine, bilgisayarımıza beklerken sıradaki isteği atmasını söyleyeceğiz.
Normalde Go'da doSomething() fonksiyonunu çağırdığımızda dönüş yapması için bekleriz (dönecek bir değeri olmasa bile yine de bitmesi için bekleriz). Bu işleme blocking işlemi diyoruz - Bitmesi için bizi bekletiyor. Go'da blocklamayan operasyon, goroutine olarak isimlendirilen ayrı bir process'te çalışır. Process'i, Go kod sayfasının yukarıdan aşağıya okunmasi gibi düşünün, fonksiyonun ne yaptığını okumak için her bir fonksiyonun 'içine' gitmek gibi düşünün. Ayrı bir process başladığında, başka bir okuyucu fonksiyonun içini okurken orjinal okuyucu sayfanın aşağısında gitmeye devam etmesine benziyor.
Go'ya yeni bir goroutine başlatmasını söylemek için fonksiyonun önüne go keywordu koyuyoruz: go doSomething()
1
package concurrency
2
3
type WebsiteChecker func(string) bool
4
5
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
6
results := make(map[string]bool)
7
8
for _, url := range urls {
9
go func() {
10
results[url] = wc(url)
11
}()
12
}
13
14
return results
15
}
Copied!
goroutine başlatmanın tek yolu fonksiyon çağrısının önüne go koymak, goroutine başlatmak istediğimizde genellikle anonymus fonksiyonları kullanıyoruz. anonymus fonksiyon literali, normal fonksiyon tanımı ile aynı, sadece isimsiz(şaşırtmayacak şekilde). Yukarıda for döngüsünün içinde görebilirsiniz.
Anonymus fonksiyonlar onları kullanışlı yapan bir dizi özelliklere sahipler, ikisini yukarıda kullandık. İlk olarak, tanımlandıkları anda çalıştırılabilmeleri - anonymus fonksiyonunun sonundaki () işareti bunu yapmakta. İkinci olarak, tanımlandıkları lexical scope'a erişim sağlarlar - anonymus fonksiyonu tanımladığınuz noktada mevcut olan tüm değişkenler, aynı zamanda fonksiyonun body'sinden de erişilebilirler
Yukarıdaki anonymus fonksiyonun body'si, loop'un body'sinin önceki hali ile aynı. Tek fark, loop'un her iterasyonda, var olan process ile concurrent olan (WebsiteChecker fonksiyonu), her birinin sonucunu result map'e ekleyen yeni bir goroutine başlatması.
Ancak, go test çalıştırdığımızda:
1
--- FAIL: TestCheckWebsites (0.00s)
2
CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[]
3
FAIL
4
exit status 1
5
FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s
Copied!

Paralel(izm) evrenine hızlı bir geçiş...

Bu sonucu alamayabilirsiniz. İleride konuşacağımız olan panic mesajını almış olabilirsiniz. Bunu alırsanız endişe etmeyin, sadece yukarıdaki sonucu alana kadar testi çalıştırmaya devam edin. Ya da yaptığınızı farz edin. Size kalmış. Concurrency'ye hoş geldiniz: doğru şekilde ele alınmadığında ne olacağını tahmin etmek zor. Endişe etmeyin - Bu nedenle test yazıyoruz,

... ve, geri döndük.

Orijinal testler tarafından yakalandık CheckWebsites şimdi boş bir harita döndürüyor. Ne yanlış gitmiş olabilir?
for loopumuz başladığında, goroutinelerin hiçibiri sonuçlarını results map'e ekleyecek kadar zaman bulamıyor; WebsiteChecker fonksiyonu onlar için çok hızlı, hala boş map dönüyor.
Bunu düzeltmek için, goroutinelerin tümünün işlerini yapmalarını bekleyebiliriz ve sonra geri dönebiliriz. İki saniye bunu yapmalı, değil mi?
1
package concurrency
2
3
import "time"
4
5
type WebsiteChecker func(string) bool
6
7
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
8
results := make(map[string]bool)
9
10
for _, url := range urls {
11
go func() {
12
results[url] = wc(url)
13
}()
14
}
15
16
time.Sleep(2 * time.Second)
17
18
return results
19
}
Copied!
Şimdi, testleri çalıştırdığımızda elde edeceğiniz (veya etmeyeceğiniz - yukarıya bakın):
1
--- FAIL: TestCheckWebsites (0.00s)
2
CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[waat://furhurterwe.geds:false]
3
FAIL
4
exit status 1
5
FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s
Copied!
Bu harika değil - neden sadece bir sonuç? Belki, bekleme süresini artırarak bunu düzeltebiliriz - isterseniz deneyin. Çalışmayacaktır. Problem şu, url değişkeni for loop'un her iterasyonunda tekrar tekrar kullanılmakta - her defasında urls'ten yeni bir değer alıyor. Ancak, her goroutine url değişkeninin referansına sahip - Kendi bağımsız koyalarına sahipr değiller. Böylece, hepsi iterasyonun sonundaki url'in sahip olduğu değeri yazdırıyor - son url. Bu yüzden elimizdeki tek sonuç son url.
Bunu düzeltmek için:
1
package concurrency
2
3
import (
4
"time"
5
)
6
7
type WebsiteChecker func(string) bool
8
9
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
10
results := make(map[string]bool)
11
12
for _, url := range urls {
13
go func(u string) {
14
results[u] = wc(u)
15
}(url)
16
}
17
18
time.Sleep(2 * time.Second)
19
20
return results
21
}
Copied!
Her bir anonymus fonksiyon için url parametresi vererek - u - ve ardından url parametresi ile anonymus fonksiyonu çağırarak, goroutini başlattığımız loop'un iterasyonu için u değerinin url değeri olarak sabitlendiğinden emin oluruz. u, url değerinin kopyası, bu sayede değiştirilemez.
Eğer şanslı iseniz, aşağıdakini elde edeceksiniz:
1
PASS
2
ok github.com/gypsydave5/learn-go-with-tests/concurrency/v1 2.012s
Copied!
Eğer şanssızsanız (Benchmark çalıştırarak daha çok deneyeceğiniz için elde etmeniz daha olası)
1
fatal error: concurrent map writes
2
3
goroutine 8 [running]:
4
runtime.throw(0x12c5895, 0x15)
5
/usr/local/Cellar/go/1.9.3/libexec/src/runtime/panic.go:605 +0x95 fp=0xc420037700 sp=0xc4200376e0 pc=0x102d395
6
runtime.mapassign_faststr(0x1271d80, 0xc42007acf0, 0x12c6634, 0x17, 0x0)
7
/usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:783 +0x4f5 fp=0xc420037780 sp=0xc420037700 pc=0x100eb65
8
github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1(0xc42007acf0, 0x12d3938, 0x12c6634, 0x17)
9
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x71 fp=0xc4200377c0 sp=0xc420037780 pc=0x12308f1
10
runtime.goexit()
11
/usr/local/Cellar/go/1.9.3/libexec/src/runtime/asm_amd64.s:2337 +0x1 fp=0xc4200377c8 sp=0xc4200377c0 pc=0x105cf01
12
created by github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker
13
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xa1
14
15
... korkutucu satırların olduğu daha fazla olduğu metinler ...
Copied!
Bu uzun ve korkutucu, ama yapmamız gereken tek şey nefes almak ve stacktrace'i okumak:fatal error: concurrent map writes. Bazen, testlerimizi çalıştırdığımızda, goroutinelerin ikisi results map'e gerçkten de aynı anda yazar. Go'da mapler, aynı anda birden fazla şeyin kendilerine yazmaya çalışmasını sevmez, sonuç olarak fatal error.
Buna race condition denir, yazılımın çıktısının zamanlamaya ve kontorlümüzün olmadığı ardışık olayların bağlı olduğu buglardır. Hangi goroutine'in results map'e ne zaman yazacağımızı kontrol edemediğimiz için, aynı anda map'e yazan iki goroutine'e karşı savunmasızız.
Go, built in race detector'ü ile race conditionları testpit etmemize yardımcı olur. Bu özelliği etkinleştirmek için, testleri race flagi ile çalıştırın: go test -race
Buna benzer çıktılar elde etmelisiniz:
1
==================
2
WARNING: DATA RACE
3
Write at 0x00c420084d20 by goroutine 8:
4
runtime.mapassign_faststr()
5
/usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0
6
github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1()
7
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82
8
9
Previous write at 0x00c420084d20 by goroutine 7:
10
runtime.mapassign_faststr()
11
/usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0
12
github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1()
13
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82
14
15
Goroutine 8 (running) created at:
16
github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker()
17
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4
18
github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker()
19
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad
20
testing.tRunner()
21
/usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c
22
23
Goroutine 7 (finished) created at:
24
github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker()
25
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4
26
github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker()
27
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad
28
testing.tRunner()
29
/usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c
30
==================
Copied!
Detayları okuması, tekrardan, zor - ama WARNING: DATA RACE oldukça açık. Hatanın gövdesini okurken, bir harita üzerinde yazma gerçekleştiren iki farklı goroutin görebiliriz:
Write at 0x00c420084d20 by goroutine 8:
aynı memory bloğuna yazıyor
Previous write at 0x00c420084d20 by goroutine 7:
Bunun üzerinde, yazma işleminin hangi kod satırında olduğunu görebiliriz:
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12
goroutine 7 ve 8'in başladığı kod satırı:
/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11
Bilmeniz gereken her şey terminalinizde yazdırılır - tek yapmanız gereken onu okumak için sabırlı olmaktır.

Channellar

Bu data race'i channelları kullanarak goroutienleri koordine ederek çözebiliriz. Channellar Go'da değer alan ve gönderen veri yapılarıdır. Bu operasyonlar, detayları ile birlikte, farklı processler arasında haberleşemyi sağlar.
Bu durumda, parent process ile, url ile WebsiteChecker fonksiyonunu çalıştırma işini yapan, her bir goroutine'i düşünmek istiyorum.
1
package concurrency
2
3
type WebsiteChecker func(string) bool
4
type result struct {
5
string
6
bool
7
}
8
9
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
10
results := make(map[string]bool)
11
resultChannel := make(chan result)
12
13
for _, url := range urls {
14
go func(u string) {
15
resultChannel <- result{u, wc(u)}
16
}(url)
17
}
18
19
for i := 0; i < len(urls); i++ {
20
r := <-resultChannel
21
results[r.string] = r.bool
22
}
23
24
return results
25
}
Copied!
results map'in yanında artık resultChannel var, aynı make ile oluşturduğumz gibi. chan result channel tipi -result channel'ı. Yeni tip, result, WebsiteChecker'ın dönüş değerini kontrol edilen url ile ilişkilendirmek için yapıldı
  • string ve bool yapısıdır. Adlandırılacak her iki değere de ihtiyacımız olmadığı için, her bir yapı içinde anonimdir; Değerlerin isimlendirmek zor olduğunda oldukça kullanışlı olabiliyor.
Url'ler üzerinde iterate ettiğimizde, map'e doğrudan yazmak yerine, wc'ye yapılan her çağrıyı result yapısını resultChannel'a send statement ile gönderiyoruz. Bu <- operatörünü kullanır, sol tarafına channel ve sağ tarafına değeri alır:
1
// Send statement
2
resultChannel <- result{u, wc(u)}
Copied!
Sıradaki for loop, her bir url'i iterate eder. İçerisinde, değeri bir channel'dan alan ve bir değişkene atayan , receive expression kullanıyoruz. Bu da <- operatörünü kullanır ama iki operand yer değiştirdi: channel sağda ve atayacağımız deişken de solda:
1
// Receive expression
2
r := <-resultChannel
Copied!
Daha sonra map'i güncellemek için elde edilen result'ı kullanıyoruz.
Sonuçları bir channel'a göndererek, results map'ine yapılan yazma işlemlerinin zamanlamasını kontrol ediyoruz, sadece bir kere gerçekleştiğinden emin oluyoruz. wc çağrılarının her biri ve result channel'ına gönderilen her bir çağrı kendi processi içinde paralel olarak gerçekleşse de, sonuç kanalından receive expression ile değerleri birer birer çıkarıyoruz.
Kodun daha hızlı yapmak istediğimiz kısmını paralel hale getirdik ve paralel olarak gerçekleşmeyen kısmın hala lineer olarak gerçekleşmesini sağladık. Channelları kullanarak çoklu processler arasında iletişim kurduk.
Benchmark'ı çalıştırdığımızda:
1
pkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v2
2
BenchmarkCheckWebsites-8 100 23406615 ns/op
3
PASS
4
ok github.com/gypsydave5/learn-go-with-tests/concurrency/v2 2.377s
Copied!
23406615 nanosaniye - 0.023 saniye, orijinal fonksiyondan yüz kat daha hızlı. Harika bir başarı.

Özetlersek

Bu egzersiz TDD'de normalden biraz daha hafif oldu. Bir bakıma, CheckWebsites fonksiyonunun uzun bir yeniden düzenlemesinde yer alıyoruz; girdiler ve çıktılar asla değişmedi, sadece daha hızlı oldu. Ama yazdığımız testler, yazdığımız benchmark yanı sıra, CheckWebsites'ı hala çalıtşığına dair güveni sağlayacak şekilde refactor etmemizi sağladı, aslında daha hızlı olduğunu gösteriyor.
Daha hızlı yapmak için öğrendiklerimiz
  • goroutines, Go'da concurrency'nin basit bir birimi, aynı anda birden fazla web sitesi kontrol etmemizi sağladı.
  • anonymous functions, web sitelerini kontrol eden concurrent processleri başlatmamızda kullandık.
  • channels, farklı processler arasında iletişimi kontrol ve organize etmemize yardımcı eder, race condition bug'ında kaçınmamızı sağlar.
  • the race detector concurrent kodda problemleri debug etmemize yardım etti

Daha hızlı yap

Yazılım geliştirmenin çevik bir yolunun formülasyonu, genellikle Kent Beck'e atfedilir:
'Çalışma' testleri geçmek, 'doğru' kodu refactor etmek ve 'hızlı' kodu çabucak çalıştırması için optimize etmektir. Bir kere çalışır ve doğru hale getirdikten sonra sadece 'hızlı hale' getirebiliriz. Bize verilen kodun zaten çalıştığını gösterdiği için şanslıydık ve refactor etmemize gerek yoktu Önce diğer iki adım yapılmadıkça, asla 'hızlı hale getirmeyi' denememeliyiz
Last modified 1mo ago