Open7

Learn Concurrent Programming with Go読書メモ

ta.toshiota.toshio

8 Selecting channels

select文を使って複数のチャンネル操作を組み合わせると、最初にブロック解除された操作が実行される。

select文のデフォルト・ケースを使用することで、ブロッキング・チャネルでノンブロッキング動作をさせることができます。

select文でsendまたはreceiveチャネル操作とTimerチャネルを組み合わせると、指定されたタイムアウトまでのチャネルがブロックされます。

select文はメッセージの受信だけでなく、送信にも使える。

nilチャネルに送信しようとしたり、nilチャネルから受信しようとすると、実行がブロックされます。

Selectケースは、nilチャネルを使用する場合は無効にすることができます。

メッセージ・パッシングは、よりシンプルで理解しやすいコードを生成する。

コードの結合が密だと、新しい機能を追加するのが難しいアプリケーションになる。

疎結合で書かれたコードは保守しやすい。

メッセージパッシングを使った疎結合のソフトウェアは、メモリ共有を使うよりもシンプルで読みやすい傾向がある。

メッセージパッシングを使用した並行アプリケーションは、各実行が共有された状態ではなく、独自の分離された状態を持つため、より多くのメモリを消費する可能性がある。

大きなデータ・チャンクの交換を必要とする並行アプリケーションは、メモリ共有を使った方がよいかもしれません。メッセージ・パッシングのためにデータをコピーすると、パフォーマンスが大幅に低下する可能性があるからです。

メモリ共有は、メッセージパッシングを使うと膨大な数のメッセージをやり取りするアプリケーションにより適している。

ta.toshiota.toshio

9 Programming with channels

  • Introducing communicating sequential processes
  • Reusing common channel patterns
  • Taking advantage of channels being first-class objects

9.1 Communicating sequential processes

共有データを更新する必要があるときにコピーを作成すると、次のような問題が生じる。メモリの別の場所にある新しい更新データをどのように共有するか?メモリの別の場所にある、新しく更新されたデータをどのように共有するのか?そこで、メッセージ・パッシングとCSPが役に立つ。

9.2 Reusing common patterns with channels

9.2.2 Pipelining with channels and goroutines

ゴルーチンをつなげて実行パイプラインを形成するパターン

func main() {
    quit := make(chan int)
    defer close(quit)
    results := extractWords(quit, downloadPages(quit, generateUrls(quit)))
    for result := range results {
        fmt.Println(result)
    }
}

results := extractWords(quit, downloadPages(quit, generateUrls(quit)))
の部分がパイプラインのように動作している。読み取りのチャネルを引数にとり、返り値にそれを返す作りで、連鎖できるようにしている。(デコレーターパターンに似ていると思う)

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter9/listing9.7_8/extractwords.go

9.2.3 Fanning in and out

ファンアウト同時実行パターンとは、複数のゴルーチンが同じチャネルから読み込む場合である。このようにして、一連のゴルーチンに作業を分散させることができる。

const downloaders = 20

func main() {
    quit := make(chan int)
    defer close(quit)
    urls := generateUrls(quit)
    pages := make([]<-chan string, downloaders)for i := 0; i < downloaders; i++ {            ❷
        pages[i] = downloadPages(quit, urls)}. . .

downloadPagesという複数のゴルーチンが同じurlsというチャネルから読み込みをしている

ファン・イン同時実行パターンは、複数のチャンネルのコンテンツを1つに統合するときに発生する。

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter9/listing9.10/fanIn.go

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter9/listing9.9_11/extractwordsmulti.go

9.2.5 Broadcasting to multiple goroutines

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter9/listing9.14/broadcast.go

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter9/listing9.16_17/wordstats.go

9.2.6 Closing channels after a condition

doneパターンのようなものだと思う。
この章ではx個チャネルから受け取ったら「閉じる」Takeというメソッドを紹介

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter9/listing9.18/take.go

9.2.7 Adopting channels as first-class objects

サンプルコードの動作が理解できない!
https://go.dev/play/p/H32JWhfopd0

Summary

CSP(Communicating Sequential Process)は、同期チャネルを介したメッセージパッシングを使用する形式言語の並行性モデルである。

CSPの実行は、それぞれ独立した状態を持ち、他の実行とメモリを共有しない。

GoはCSPからコアとなるアイデアを借用しているが、チャンネルをファーストクラスのオブジェクトとして扱っている。

quitチャネルパターンは、ゴルーチンに実行停止を通知するために使用できます。

ゴルーチンが入力チャネルを受け入れ、出力を返すという共通のパターンを持つことで、パイプラインの様々なステージを簡単に接続することができる。

ファンインパターンは複数の入力チャネルを1つにマージする。このマージされたチャネルは、すべての入力チャネルがクローズされた後にのみクローズされる。

ファンアウトパターンは、複数のゴルーチンが同じチャネルから読み出すパターンである。この場合、チャネル上のメッセージはゴルーチン間で負荷分散される。

ファンアウト・パターンは、メッセージの順序が重要でない場合にのみ意味を持つ。

ブロードキャスト・パターンでは、入力チャネルの内容が複数のチャネルに複製されます。

Goでは、チャンネルがファーストクラスのオブジェクトとして動作するということは、メッセージパッシング型並行プログラムの構造を、プログラムの実行中に動的に変更できるということを意味します。

ta.toshiota.toshio

10 Concurrency patterns

10.1 task decomposition and data decomposition

タスク分割とデータ分割は、並行プログラムを設計する際に一緒に適用すべき原則である。ほとんどの並行アプリケーションは、効率的なソリューションを実現するために、タスク分解とデータ分解を混合して適用している。

10.2 Concurrency implementation patterns

10.2.1 Loop-level parallelism

goroutingを使ってloopを順繰りに。(あれ、意味あるの?)

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter10/listing10.4/dirhashconcurrent.go

10.2.2 The fork/join pattern

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter10/listing10.5_6_7_8/deepestnestedfile.go

10.2.3 Using worker pools

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter10/listing10.12/httpservernonblocking.go

10.2.4 Pipelining

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter10/listing10.13/cupcakepipeline.go

https://github.com/cutajarj/ConcurrentProgrammingWithGo/blob/main/chapter10/listing10.15_16/cupcakefactory.go

10.2.5 Pipelining properties

ヒント システムのスループットを向上させるには、常にそのシステムのボトルネックに焦点を当てるのが最善です。これは、パフォーマンスを低下させる最大の原因となっている部分です。

Summary

分解とは、プログラムをさまざまな部分に分割し、どの部分が同時に実行できるかを見つけ出すプロセスである。

依存関係グラフの構築は、どのタスクが他のタスクと並行して実行できるかを理解するのに役立つ。

タスク分解とは、問題を、仕事全体を完了するために必要なさまざまなアクションに分解することである。

データ分解とは、データに対するタスクを並行して実行できるようにデータを分割することです。

プログラムを分解する際に細かい粒度を選択することは、同期や通信に費やす時間によってスケーラビリティが制限される代償として、より多くの並列性を意味する。

粗い粒度を選ぶと並列度は下がるが、同期や通信の量は減る。

ループレベルの並列性は、タスクに依存関係がなければ、タスクのリストを同時に実行するために使用できる。

ループレベル並列では、問題を並列部分と同期部分に分割することで、前のタスクの反復に依存することができる。

Fork/joinは、最初の並列部分と、さまざまな結果をマージする最終ステップを持つ問題がある場合に使用できる並行性パターンである。

ワーカープールは、並行性をオンデマンドでスケールする必要がある場合に有用である。

ほとんどの言語では、ワーカープールで事前に実行を作成する方が、その場で作成するよりも高速です。

Goでは、ワーカープールを事前に作成するパフォーマンスと、必要に応じてゴルーチンを動的に作成するパフォーマンスは、ゴルーチンの軽量性のため、ほとんど差がありません。

ワーカー・プールは、予期せぬ需要の増加時にサーバーに過負荷をかけないように、同時実行を制限するために使用できます。

パイプラインは、各タスクが前のタスクの完了に依存する場合に、スループットを向上させるのに有用である。

パイプラインの中で最も遅いノードの速度を上げると、パイプライン全体のスループット性能が向上します。

パイプラインのどのノードの速度を上げても、パイプラインの待ち時間は短縮されます。

ta.toshiota.toshio

11 Avoiding deadlocks

Our resource allocation graphを書いたときサイクル循環の関係になっているとデッドロック発生するよね、という解説

11.2 Dealing with deadlocks

11.2.3 Preventing deadlocks

順番にロックをとる。例えば、降順だったら降順にロックを取るルールを厳守する。

11.3 Deadlocking with channels

sumary

デッドロックとは、プログラムが複数の実行を無期限にブロックし、互いがそれぞれのリソースを解放するのを待つことである。

リソース割り当てグラフ(RAG)は、実行がどのようにリソースを使用しているかを、実行をエッジで結ぶことで示す。

RAGでは、リソースを要求する実行は、実行からリソースへの有向エッジで表される。

RAGでは、リソースを保持する実行は、リソースから実行への有向エッジで表される。

RAGがサイクルを含む場合、システムがデッドロックに陥っていることを意味する。

デッドロックを検出するために、RAG上でグラフサイクル検出アルゴリズムを使用することができる。

Goのランタイムはデッドロック検出機能を提供するが、デッドロックを検出するのは、すべてのゴルーチンがブロックされた場合だけである。

Goのランタイムがデッドロックを検出すると、プログラム全体がエラーで終了する。

特定の方法で実行をスケジューリングすることによってデッドロックを回避することは、どのリソースが使われるかがあらかじめわかっている特別な場合にのみ可能です。

デッドロックは、あらかじめ定義された順序でリソースを要求することで、プログラム的に防ぐことができる。

デッドロックは、Goチャンネルを使用しているプログラムでも発生する可能性がある。チャネルの容量は、互いに排他的なリソースと考えることができます。

ta.toshiota.toshio

12 Atomics, spin locks, and futexes

12.1 Lock-free synchronization with atomic variables

12.1.1 Sharing variables with atomic numbers

12.2 Implementing a mutex with spin locks

12.2.1 Comparing and swapping

楽観的ロックの紹介 - CompareAndSwap

12.2.2 Building a mutex

スピン・ロックとはロックの一種で、ロックが使用可能になるまで、実行がループに入
り、繰り返しロッ クを取得しようとするものである。

リソースの競合とは、ある実行(スレッド、プロセス、またはゴルーチンなど)が、他の実行をブロックして遅くするような方法でリソースを使用することである。

12.3 Improving on spin locking

12.3.1 Locking with futexes