この記事では、実際の実戦から始まり、スレッド間の競合とスレッド間の連携という2 つの観点から GO 言語の同時実行の実践を紹介し、最後に GO 言語の同時実行の本質である GO でのチャネルの使用に焦点を当てます。はじめましょう!
1 スレッド間の競合
複数のスレッドが同じ高級言語で共有データに対して競合する操作を実行すると、データの不整合が発生します。「ロック」を使用すると、共有データの同時使用を回避でき、競合の問題を解決できます。しかし、デッドロックなどの「ロック」の誤った使用は新たな問題です。GO 言語では、主にミューテックス ロックと読み取り/書き込みロックを含む「ロック」の存在がサポートされています。GO は競合を回避するための他の方法もサポートしており、主流のものにはアトミック操作、MAP ロックなどが含まれます。
ミューテックスロック
ミューテックス ロックにより、同時に1 つのみが共有データにgoroutine
アクセスできるようになります。Go 言語ではパッケージを使用してミューテックス ロックを実装します。典型的な使用例は次のとおりです。sync
package main
import (
"fmt"
"sync"
"time"
)
type test struct {
x int
mutex sync.Mutex
}
func (t *test) add(){
//对t.x++操作进行加锁,避免竞争。
//如果不加锁,会出现错误的结果
t.mutex.Lock()
t.x++
t.mutex.Unlock()
}
func (t *test) get()int{
return t.x
}
func main() {
t:=test{x: 0}
for i := 0; i < 1000; i++ {
//并发的运行添加操作
go t.add()
}
//这里之所以加休止,是因为main函数也是一个特殊的go程。所以很有可能前面开启的go程还没运行完,main程已经选择输出了,也会出现错误的结果。
//此处也可以使用WaitGroup方法,后面会介绍。
time.Sleep(100)
fmt.Println(t.get())
}
読み書きロック
読み取りが多く書き込みが少ない場合は、最初に読み取り/書き込みロックを使用できます。読み取り/書き込みロックとミューテックス ロックの使用法は非常に似ています。読み取りロックでは、共有リソースを任意に読み取ることができますが、書き込みはできません。書き込みロックはミューテックス ロックです。同期ライブラリは、GO 言語で読み取りおよび書き込みロックを提供するために使用されます。典型的な使用例は次のとおりです。
package main
import (
"fmt"
"sync"
"time"
)
// 声明读写锁
var rwlock sync.RWMutex
var x int
// 写数据
func write() {
rwlock.Lock()
x += 1
rwlock.Unlock()
}
func read(i int) {
rwlock.RLock()
fmt.Println(x)
rwlock.RUnlock()
}
func main() {
go write()
for i := 0; i < 1000; i++ {
go read(i)
}
time.Sleep(100)
}
マップロック
GO のマップは同時読み取りをサポートしていますが、同時書き込みはサポートしていません。ただし、sync
同時実行をサポートするマップを使用して同時実行を実現することはできます。サンプル コードは次のとおりです。
package main
import (
"fmt"
"sync"
)
func main() {
wg := sync.WaitGroup{}
var m = sync.Map{}
// 并发写
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m.Store(i,i*i)
}(i)
}
wg.Wait()
fmt.Println(m.Load(1))
}
アトミック操作
ライブラリを使用してアトミック操作を完了し、スレッドの安全性を確保することもできますsync/atomic
。アトミック操作はユーザーモードで完了でき、ロックよりもパフォーマンスが高く、主に以下のアトミックメソッドがあります。
//加减方法
AddXxx()
//读取方法
LoadXxx()
//写入方法
StoreXxx()
//交换方法
SwapXxx()
2 スレッド間の連携
スレッド間には競合関係だけではなく、協力関係もあります。たとえば、コンシューマ/プロデューサ モデルでは、異なるスレッド間の連携が必要です。GO言語でのコラボレーションの実現方法は、条件変数などの従来の手法を利用するだけでなく、独自のチャネル機構によるコラボレーションも実現できます。次に、WaitGroup、条件変数、チャネルを順に紹介します。
待機グループ
WaitGroup を GO で使用すると、複数の GO プロセス間の同期を実現できます。型内ではsync.WaitGroup
、各 sync.WaitGroup 値は内部的に初期デフォルト値 0 のカウントを維持します。このカウンタは、次のメソッドを呼び出すことで変更できます。
メソッド名 | 関数 |
---|---|
加算(x int) | 待機グループカウンター+1 |
終わり() | 待機グループのカウンター -1 |
待って() | ウェイトグループカウンタが0以外の場合は0になるまでブロックします。 |
通常、他の GO プロセスが完了するのを待つメイン関数の GO プロセスとして使用されます。
package main
import (
"fmt"
"sync"
)
//声明一个计数器
var wg sync.WaitGroup
type test struct {
x int
mutex sync.Mutex
}
func (t *test) add(){
//运行完毕 计数器减一
defer wg.Done()
//对t.x++操作进行加锁,避免竞争。
//如果不加锁,会出现错误的结果
t.mutex.Lock()
t.x++
t.mutex.Unlock()
}
func (t *test) get()int{
return t.x
}
func main() {
t:=test{x: 0}
for i := 0; i < 1000; i++ {
//运行一个新GO程,计数器加一
wg.Add(1)
//并发的运行添加操作
go t.add()
}
//直到计数器为0,不然会一直阻塞
wg.Wait()
fmt.Println(t.get())
}
条件変数
条件変数は通常、待機/ウェイクアップ モデルを実装するためにミューテックス ロックと一緒に使用され、コラボレーションの重要な部分です。外部ウェイクアップ信号を受信するまでスレッドをブロックし、その後再度実行することができます。GO の条件変数はsync
ロックに基づいており、ライブラリに実装されています。以下は GO の条件変数の簡単な例です。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var cond *sync.Cond
func test(x int) {
defer wg.Done()
cond.L.Lock() // 获取锁
cond.Wait() // 等待通知 暂时阻塞
fmt.Println(x)
time.Sleep(time.Second * 1)
cond.L.Unlock()
}
func main() {
cond = sync.NewCond(&sync.Mutex{})
fmt.Println("start all")
for i := 0; i < 40; i++ {
wg.Add(1)
go test(i)
}
time.Sleep(time.Second * 3)
fmt.Println("one")
cond.Signal() // 下发一个通知给已经阻塞的goroutine
time.Sleep(time.Second * 3)
fmt.Println("one")
cond.Signal() // 3秒之后 下发一个通知给已经阻塞的goroutine
time.Sleep(time.Second * 3)
fmt.Println("broadcast")
cond.Broadcast() //3秒之后 下发广播给所有等待的goroutine
wg.Wait()
}
3チャンネル
共有メモリでコミュニケーションするのではなく、コミュニケーションでメモリを共有する
上で紹介したロックとアトミック操作はほとんどのプログラミング言語でサポートされていますが、次に、GO 言語の同時実行コア コンポーネント チャネル (パイプライン) を紹介します。
チャネルとゴルーチンは相互に補完します。GO の独自の同時実行性として、GO プロセスはユーザー モード コルーチンであり、非常に軽量であり、平均的なパフォーマンスのコンピューター上で数千の GO プロセスを簡単に実行できます。Channel の主な機能は、GO プロセス間の通信ブリッジを提供する通信チャネルとして機能することであり、グローバルな同時安全キューと同様に、データを受信および送信する機能があります。GO 言語の設計者は、共有メモリを介して通信するのではなく、通信を介してメモリを共有することを重視しています。これが GO 言語の設計思想です。チャネルの使用については次に紹介します。
チャネルを定義して作成する
チャネルの定義はマップと非常に似ています。まず、チャネル内のデータ構造は自分で定義することができ、例えば次のコードではInt型を格納できるチャネルを定義しています。
var ch chan int
上記のコードに注目し、いくつかの点に注意してください。
- ch を int 型の chan として定義した後でも、ch は nil 値のままであり、直接使用することはできません。マップの使用と同様に、後で作成するには makeを使用する。
- int 型の chan を定義できるだけでなく、他の型の chan も定義できます。
チャネルの作成もマップと非常に似ており、次のコードを使用して作成できます。
ch=make(chan int,10)
知らせ:
- 作成時に指定した型と定義時に指定した型は一致している必要があり、Chan は作成後にのみ正常に使用できます。
- 作成時にチャネルのサイズを指定できます。上記の例では 10 です。サイズが 0 の場合、作成されたチャネルはバッファリングされていないチャネルとなり、0 より大きい場合、作成されたチャネルはバッファリングされます。
もちろん、定義と作成を一緒に行うこともできます。
ch:=make(chan int,10)
チャンネルの基本操作
チャネルにはデータの送信と受信の機能があり、書き込み方法は以下のとおりです。
x := 0
//创建通道ch
ch:= make(chan int,1)
// 向ch发送数据
ch <- 1
// 从ch接收数据
x = <- ch
知らせ:
- 各チャンネルにはバッファサイズがあり、バッファがいっぱいになると送信がブロックされ、バッファが空になると受信がブロックされます。
- バッファリングされていないチャネルの 1 回の送信と 1 回の受信はブロックされ、送信と受信のペアが存在する場合にのみ操作を続行できます。
パイプラインは、チャネルの動作分類に応じて、通常チャネルと片方向チャネルに分類することもでき、以下のように定義されます。
//只能向通道发送数据
var ch chan-< int
//只能从通道接受数据
var ch <-chan int
- 一般に、チャネルを関数値として関数に渡すときは、一方向チャネルが使用され、チャネル上での関数の操作が制限されます。
次に、チャネルの使用法を示すために、卓球をする 2 人の例を作成します。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func A(ch chan int) {
defer wg.Done()
for {
time.Sleep(time.Second)
//得到击球机会
num := <-ch
//如果是有效的回球
if num >= 1 {
//产生随机数回球,如果数小于300,代表没打到球,输掉比赛。
temp := rand.Int63() % 1000
fmt.Println("A击出第", num, "球,得到", temp, "分!")
if temp < 300 {
fmt.Println("A输掉比赛!")
ch <- 0
break
}
ch <- num + 1
} else {
//如果是无效的球,退出循环
break
}
}
fmt.Println("A结束比赛~")
}
//B和A类似
func B(ch chan int) {
defer wg.Done()
for {
time.Sleep(time.Second)
//得到击球机会
num := <-ch
if num >= 1 {
temp := rand.Int63() % 1000
fmt.Println("B击出第", num, "球,得到", temp, "分!")
if temp < 300 {
fmt.Println("B输掉比赛!")
ch <- 0
break
}
ch <- num + 1
} else {
break
}
}
fmt.Println("B结束比赛~")
}
func main() {
ch := make(chan int)
wg.Add(2)
//启动双方
go A(ch)
go B(ch)
//发球
ch <- 1
//等待比赛结束
wg.Wait()
}
選択する
chan の基本操作を学んだ後は、非常に重要なこと、つまり選択を学ぶ必要があります。select と switch の構文は非常に似ていますが、使用方法はまったく異なります。これは通信専用のスイッチとして理解でき、それぞれの場合はチャネル通信操作 (受信または送信) である必要があります。
注: Go 言語
select
ステートメントは Unix 関数から借用したものです。Unix では、関数を呼び出すことで一連のファイル ハンドルを監視できます。ファイル ハンドルの 1 つで IO アクションが発生すると、呼び出しがselect()
返されます (これは、 C 言語の場合 (完了)、その後、このメカニズムは同時実行性の高いソケット サーバー プログラムの実装にも使用されました。Go 言語は、言語レベルでキーワードを直接サポートし、同時プログラミングにおけるチャネル間の非同期 IO 通信の問題を処理します。——『Huben GO(21世紀のC言語)』select()
select()
select
基本的な構文は次のとおりです。
select {
case <-ch:
// 如果从 ch 信道成功接收数据,则执行该分支代码
case ch1 <- 1:
// 如果成功向 ch1 信道成功发送数据,则执行该分支代码
// 你可以定义任意数量的 case
//default为可选项
default :
//如果没有case可运行,则运行此分支
}
いくつかの点に注意してください。
-
各ケースの後には、chan に関する通信操作が続く必要があります。
-
case に続くすべての式が実行されるわけではありません。実行可能なケースが複数ある場合、select は実行可能なケースをランダムに実行し、その下の分岐コードを実行します。
-
実行するケースがない場合は、デフォルトのブランチが実行されます。デフォルトのブランチがない場合は、実行するケースが見つかるまでブロックされます。
選択とタイマー
Select は、定期的に特定の操作を実行するためにタイマーと併用されることがよくあります。通常、指定された時間内にサービスが完了するかどうかを監視するために使用されます。変更できる簡単なコードを次に示します。
package main
import (
"fmt"
"time"
)
func A(ch chan int) {
time.Sleep(time.Second * 2)
ch <- 1
}
func main() {
ch := make(chan int)
go A(ch)
select {
//接受来自ch的消息
case <-ch:
fmt.Println("接收到消息!")
//定时器设置为1s,超过1s后case被激活
case <-time.After(1 * time.Second):
fmt.Println("没接收到消息!")
}
}
要約する
この記事では、主に GO 言語の同時実行の基本的な構文を紹介し、いくつかの小さなデモを示します。著者の GO 言語コラムには、高度なバージョンの実践例もいくつか掲載されているので、興味のある方は、ホームページにアクセスして「GO 同時実行性」を検索してご覧ください。