Wann man sync vs. channel in Go verwendet
Emily Parker
Product Engineer · Leapcell

Wie man zwischen sync
und channel
wählt
Bei der Programmierung in C verwenden wir im Allgemeinen gemeinsam genutzten Speicher für die Kommunikation. Wenn mehrere Threads gleichzeitig auf ein gemeinsam genutztes Datenelement zugreifen, verwenden wir Mutexe, um bei Bedarf zu sperren und zu entsperren, um Datensicherheit zu gewährleisten und die Thread-Synchronisierung zu steuern.
In Go wird jedoch empfohlen, Speicher durch Kommunikation zu teilen – indem Kanäle verwendet werden, um die Synchronisierungsmechanismen für kritische Abschnitte zu vervollständigen.
Allerdings sind Kanäle in Go relativ hochlevelige Primitive und haben naturgemäß eine geringere Leistung im Vergleich zu den Sperrmechanismen im sync
-Paket. Bei Interesse können Sie selbst einen einfachen Benchmark-Test schreiben, um ihre Leistung zu vergleichen, und Ihre Ergebnisse in den Kommentaren diskutieren.
Darüber hinaus verlieren Sie bei der Verwendung des sync
-Pakets zur Steuerung der Synchronisierung nicht das Eigentum am Struct-Objekt, und Sie können weiterhin mehreren Goroutinen den synchronisierten Zugriff auf kritische Abschnittsressourcen ermöglichen. Wenn Ihre Anforderungen in dieses Szenario passen, wird daher weiterhin empfohlen, das sync
-Paket zur Synchronisierung zu verwenden, da es sinnvoller und effizienter ist.
Warum Sie das sync
-Paket für die Synchronisierung wählen sollten:
- Wenn Sie die Kontrolle über das Struct nicht verlieren möchten und gleichzeitig mehreren Goroutinen einen sicheren Zugriff auf kritische Abschnittsressourcen ermöglichen möchten.
- Wenn eine höhere Leistung erforderlich ist.
sync
's Mutex und RWMutex
Wenn wir uns den Quellcode des sync
-Pakets ansehen, stellen wir fest, dass er die folgenden Strukturen enthält:
- Mutex
- RWMutex
- Once
- Cond
- Pool
- Atomare Operationen im
atomic
-Paket
Unter diesen ist Mutex
am häufigsten verwendet, besonders wenn Sie noch nicht geübt im Umgang mit Kanälen sind; Sie werden Mutex sehr nützlich finden. Im Gegensatz dazu wird RWMutex
seltener verwendet.
Haben Sie jemals auf den Leistungsunterschied zwischen der Verwendung von Mutex
und RWMutex
geachtet? Die meisten Leute verwenden standardmäßig einen Mutex. Schreiben wir also eine einfache Demo, um ihre Leistung zu vergleichen.
var ( mu sync.Mutex murw sync.RWMutex tt1 = 1 tt2 = 2 tt3 = 3 ) // Verwenden von Mutex zum Lesen von Daten func BenchmarkReadMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { mu.Lock() _ = tt1 mu.Unlock() } }) } // Verwenden von RWMutex zum Lesen von Daten func BenchmarkReadRWMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { murw.RLock() _ = tt2 murw.RUnlock() } }) } // Verwenden von RWMutex zum Lesen und Schreiben von Daten func BenchmarkWriteRWMutex(b *testing.B) { b.RunParallel(func(pp *testing.PB) { for pp.Next() { murw.Lock() tt3++ murw.Unlock() } }) }
Wir haben drei einfache Benchmark-Tests geschrieben:
- Lesen von Daten mit einer Mutex-Sperre.
- Lesen von Daten mit der Lesesperre einer Lese-/Schreibsperre.
- Lesen und Schreiben von Daten mit einer Lese-/Schreibsperre.
$ go test -bench . bbb_test.go --cpu 2 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-2 39638757 30.45 ns/op BenchmarkReadRWMutex-2 43082371 26.97 ns/op BenchmarkWriteRWMutex-2 16383997 71.35 ns/op $ go test -bench . bbb_test.go --cpu 4 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-4 17066666 73.47 ns/op BenchmarkReadRWMutex-4 43885633 30.33 ns/op BenchmarkWriteRWMutex-4 10593098 110.3 ns/op $ go test -bench . bbb_test.go --cpu 8 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-8 8969340 129.0 ns/op BenchmarkReadRWMutex-8 36451077 33.46 ns/op BenchmarkWriteRWMutex-8 7728303 158.5 ns/op $ go test -bench . bbb_test.go --cpu 16 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-16 8533333 132.6 ns/op BenchmarkReadRWMutex-16 39638757 29.98 ns/op BenchmarkWriteRWMutex-16 6751646 173.9 ns/op $ go test -bench . bbb_test.go --cpu 128 goos: windows goarch: amd64 cpu: Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz BenchmarkReadMutex-128 10155368 116.0 ns/op BenchmarkReadRWMutex-128 35108558 33.27 ns/op BenchmarkWriteRWMutex-128 6334021 195.3 ns/op
Aus den Ergebnissen können wir ersehen, dass die Leistung von Mutex-Sperren und Lesesperren (von RWMutex) ähnlich ist, wenn die Parallelität gering ist. Mit zunehmender Parallelität ändert sich die Leistung der Lesesperre in RWMutex nicht wesentlich, aber die Leistung sowohl der Mutex als auch der RWMutex verschlechtert sich mit zunehmender Parallelität.
Es ist klar, dass RWMutex für leseintensive, schreibschwache Szenarien geeignet ist. In Szenarien mit vielen gleichzeitigen Lesevorgängen können mehrere Goroutinen gleichzeitig die Lesesperre erhalten, was die Sperrenkonflikte und Wartezeiten reduziert.
Bei einem regulären Mutex kann jedoch immer nur eine Goroutine die Sperre gleichzeitig unter Parallelität erhalten. Andere Goroutinen werden blockiert und müssen warten, was sich negativ auf die Leistung auswirkt.
Sehen wir uns zum Beispiel an, welche Probleme auftreten können, wenn wir in der Praxis einen regulären Mutex verwenden.
Dinge, die Sie bei der Verwendung von sync
beachten sollten
Wenn Sie die Sperren im sync
-Paket verwenden, sollten Sie einen Mutex oder RWMutex nicht kopieren, nachdem dieser bereits verwendet wurde.
Hier ist eine einfache Demo:
var mu sync.Mutex // Kopieren Sie Mutex oder RWMutex nicht, nachdem sie verwendet wurden. // Wenn Sie kopieren müssen, tun Sie dies nur vor der Verwendung. func main() { go func(mm sync.Mutex) { for { mm.Lock() time.Sleep(time.Second * 1) fmt.Println("g2") mm.Unlock() } }(mu) mu.Lock() go func(mm sync.Mutex) { for { mm.Lock() time.Sleep(time.Second * 1) fmt.Println("g3") mm.Unlock() } }(mu) time.Sleep(time.Second * 1) fmt.Println("g1") mu.Unlock() time.Sleep(time.Second * 20) }
Wenn Sie diesen Code ausführen, werden Sie feststellen, dass `