CSP 同時実行モデルは、2 つの独立した同時エンティティが共有チャネル (パイプライン) を通じて通信する同時実行モデルを記述するために 1970 年代に提案されました。
Go 言語は同時実行性を実現するために CSP 同時実行モデルのいくつかの概念を使用しますが、Go 言語は CSP 同時実行モデルのすべての理論を完全に実装しているわけではなく、プロセスとチャネルの 2 つの概念のみを実現します。
プロセスはGo言語におけるゴルーチンであり、各ゴルーチンはチャネル通信によりデータ共有を実現します。
ここで明確にしておきたいのは、「同時実行は並列処理ではない」ということです。同時実行では、プログラムの設計レベルがより重視されます。同時プログラムは順次実行できます。実際のマルチコア CPU 上でのみ同時に実行できます。並列では、プログラムの実行レベルがより重視されます。たとえば、繰り返します。 、GPU での画像処理には多数の並列操作が発生します。
並行プログラムをより適切に作成するために、Go 言語は、設計の最初からプログラミング言語レベルで簡潔で安全かつ効率的な抽象モデルを設計する方法に焦点を当ててきました。これにより、開発者は、複雑な作業をすることなく、問題の分解と解決策の組み合わせに集中できるようになります。スレッド管理と信号のやり取りに圧倒され、エネルギーを分散させるためにこれらの面倒な操作を拒否してください。
同時プログラミングでは、共有リソースへの正しいアクセスを正確に制御する必要があります。現在のほとんどの言語では、この困難な問題を解決するために、ロックなどのスレッド同期ソリューションが使用されます。 の値は、チャネル (実際には、複数の独立して実行されるチャネル) を介して渡されます。スレッドが積極的にリソースを共有することはほとんどありません)。
並行プログラミングの中核となる概念は同期通信ですが、同期にはさまざまな方法があります。まずはおなじみのmutex sync.Mutexを使って同期通信を実現するサンプルコードは以下の通りです。
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
go func() {
fmt.Println("IT基礎")
mu.Lock()
}()
mu.Unlock()
}
mu.Lock() と mu.Unlock() は同じゴルーチン内にないため、逐次一貫性メモリ モデルは満たされません。同時に、参照する他の同期イベントがないため、これら 2 つは同時に実行できることになります。
これは同時イベントである可能性があるため、main() 関数の mu.Unlock() が最初に発生する可能性が高く、この時点では mu ミューテックスはまだロック解除されているため、実行時例外が発生します。
修正されたコードは次のとおりです。
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
go func() {
fmt.Println("IT基礎")
mu.Unlock()
}()
mu.Lock()
}
これを修正する方法は、main() 関数が配置されているスレッドで mu.Lock() を 2 回実行することです。ロックが 2 回目に追加されると、ロックは既に占有されているためブロックされます (再帰的ではありません)。 main() 関数のブロック状態により、バックグラウンド スレッドが順方向に実行され続けます。
バックグラウンド スレッドが mu.Unlock() を実行すると、ロックが解除され、この時点で印刷作業が完了します。ロックを解除すると、main() 関数の 2 番目の mu.Lock() ブロック状態が解除されます。今度は、バックグラウンド スレッドとメイン スレッドは、同期イベントへの他の参照がない場合、それらの終了イベントは同時実行され、main() 関数が終了するまでにバックグラウンド スレッドが終了している場合とそうでない場合があり、プログラムが終了します。 2 つのスレッドがいつ終了するかを判断することはできませんが、印刷ジョブは正しく実行されます。
sync.Mutex ミューテックス同期の使用は比較的低レベルのアプローチですが、ここではバッファなしのチャネルを使用して同期を実現します。
package main
import する(
"fmt"
)
func main() {
done := make(chan int)
go func() {
fmt.Println("IT基礎")
<-done
}()
done <- 1
}
Go 言語のメモリ モデル仕様によれば、バッファされていないチャネルからの受信は、そのチャネルへの送信が完了する前に発生します。したがって、バックグラウンド スレッドの<-done
受信操作が完了した後、メイン スレッドのdone <- 1
送信操作が完了する可能性があり (したがってメインを終了し、プログラムを終了します)、この時点で印刷作業は完了しています。
上記のコードは正しく同期できますが、チャネルのバッファ サイズに敏感すぎます。チャネルにバッファがある場合、main() 関数が終了する前にバックグラウンド スレッドが正常に印刷できるかどうかは保証できません。より良い方法は次のとおりです。同期イベントがチャネル バッファのサイズの影響を受けないように、チャネルの送信と受信の方向を逆に設定します。
package main
import (
"fmt"
)
func main() {
done := make(chan int, 1) // バッファ付きチャネル
gorountine: = func() {
fmt.Println("IT基礎")
done <- 1
}()
<-done
}
バッファリングされたチャネルの場合、チャネル上の K 番目の受信操作の完了は、K+C 番目の送信操作の完了前に発生します。ここで、C はチャネルのバッファ サイズです。チャネルはバッファリングされていますが、メインスレッドの受信はバックグラウンドスレッドが送信を完了していない時点で完了しており、この時点で印刷作業も完了しています。
バッファーされたチャネルに基づいて、印刷スレッドの数を N まで簡単に拡張できます。次の例では、10 個のバックグラウンド スレッドが個別に印刷できるようにしています。
package main
import (
"fmt"
)
func main() {
done := make(chan int, 10) // 10個のキャッシュを持つ
// N個のバックグラウンドプリントスレッドを開始する
for i := 0; i < cap(done); i++ {
go func() {
fmt.Println("IT基礎")
done <- 1
}()
}
// N個のバックグラウンドスレッドの完了を待つ
for i := 0; i < cap(done); i++ {
<-done
}
}
次のステップに進む前に N 個のスレッドが完了するまで待機する必要があるこの種の同期操作の場合、sync.WaitGroup を使用してイベントのグループを待機する簡単な方法があります。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// N個バックグラウンド印刷スレッドを開く
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println("IT基盤")
wg.Done()
}()
}
// N個のバックグラウンドスレッドの完了を待つ
wg.Wait()
}
このうち、wg.Add(1)は待機イベント数を増やすために使用されており、バックグラウンドスレッドが起動する前に実行する必要があります(バックグラウンドスレッドで実行した場合、正常に実行されることは保証できません)。バックグラウンド スレッドが印刷作業を完了した後、wg.Done() を呼び出してイベントの完了を示し、main() 関数の wg.Wait() はすべてのイベントが完了するのを待ちます。