今回は、Go言語の並行・並列処理のかなめともいえるgoroutine(ゴルーチン)周りを掘り下げていきます。
goroutineは、前回の記事で軽く触れたように、軽量スレッドと呼ばれるものです。 そこで、まずはこの軽量スレッドと通常のOSのスレッドがどう違うのかを説明します。
そのうえで、goroutineの低レベルな機能を扱うためのruntimeパッケージ、 goroutineのデータ競合を発見するRace Detecter、 さらに高度な非同期処理を書くときに必要になるsyncパッケージおよびsync/atomicパッケージの使い方を紹介します。
スレッドとgoroutineの違い
スレッドとは、プログラムを実行するための「もの」であり、OSによって手配されます。
プログラムから見たスレッドは、「メモリにロードされたプログラムの現在の実行状態を持つ仮想CPU」です。この仮想CPUのそれぞれに、スタックメモリが割り当てられています。
一方、OSやCPUから見たスレッドは、「時間が凍結されたプログラムの実行状態」です。この実行状態には、CPUが演算に使ったり計算結果や状態を保持したりするレジスタと呼ばれるメモリと、スタックメモリが含まれます。
OSの仕事は、凍結状態のプログラムの実行状態を復元して、各スレッドを順番に短時間ずつ処理を再開させることです。 その際の順番や、一回に実行する時間タイムスライスは、スレッドごとに設定されている優先度によって決まります。 実行予定のスレッドはランキューと呼ばれるリストに入っており、なるべく公平に処理が回る用にスケジューリングされます。 複数のプログラムは、このようにして、時間分割してCPUコアにマッピングされて実行されるのです。
スレッドがCPUコアに対してマッピングされるのに対し、goroutineはOSのスレッド(Go製のアプリケーションから見ると1つの仮想CPU)にマッピングされます。 この点が、通常のスレッドとGo言語の軽量スレッドであるgoroutineとの最大の違いです。 両者にはほかにも次のような細かい違いがあります。
- OSスレッドはIDを持つが、goroutineは指定しなければ実際のどのスレッドにマッピングされるかは決まっておらず、IDなども持たない
- OSスレッドの1〜2MBと比べると、初期スタックメモリのサイズが小さく(2KB)、起動処理が軽い
- 優先度を持たない
- タイムスライスで強制的に処理が切り替わることがないが、コンパイラが「ここで処理を切り替える」という切り替えポイントを埋め込むことで切り替えを実現している
- (IDで一意にgoroutineで特定できないため)外部から終了のリクエストを送る仕組みがない
GoのランタイムはミニOS
OSが提供するスレッド以外に、プログラミング言語のランタイムでスレッド相当の機能を持つことには、どんなメリットがあるのでしょうか。 Go言語の場合、「機能が少ない代わりにシンプルで起動が早いスレッド」として提供されているgroutineを利用できることで、次のようなメリットが生まれています。
- 大量のクライアントを効率よくさばくサーバーを実装する(いわゆるC10K)ときに、クライアントごとに1つのgoroutineを割り当てるような実装であっても、リーズナブルなメモリ使用量で処理できる
- OSのスレッドでブロッキングを行う操作をすると、他のスレッドが処理を開始するにはOSがコンテキストスイッチをして順番を待つ必要があるが、Goの場合はチャネルなどでブロックしたら、残ったタイムスライスでランキューに入った別のgoroutineのタスクも実行できる
- プログラムのランタイムが、プログラム全体の中でどのgoroutineがブロック中なのかといった情報をきちんと把握しているため、デッドロックを作ってもランタイムが検知してどこでブロックしているのかを一覧で表示できる
goroutineは、OSのスレッドと比べると機能的にはシンプルですが、OSのカーネルが行っていることとGo言語のランタイムが行っていることは、大雑把に次のように対応しています。
Go言語の機能(ランタイムにおける用語) | Linuxカーネルにおいて対応するもの |
---|---|
M: Machine | 物理CPUコア |
P: Process | スケジューラ(ランキュー) |
G: goroutine | プロセス |
つまり、Go言語のランタイムは、goroutineをスレッドとみなせば、OSと同じ構造であると考えることができます。
実際、Goのランタイムの内部には、OSがスレッドのスケジューラを持っているのと同様に、goroutineのスケジューラがあります。 具体的には、OSのスレッド(M)ごとに、タスクであるgoroutine(G)のリストがあり、実行中のスレッド上で順番にタスクをどんどん切り替えて実行していきます。 ランキューなどのスレッドが行う作業を束ねるものは、Process(P)と呼ばれます(システムプログラミングにおけるプロセスとは別物です)。 グローバルなタスクのキューもあり、暇になったプロセスはそこからタスクを取得してきます。 また、仕事に偏りがあると、それを平準化する機構も持っています。 また、現代のカーネルはCPUのM個のコア数に対して同時にN個の複数の処理ができるM:Nモデルになっていますが、Go言語のgoroutineもOSスレッドの数Mに対してM:Nモデルになっています。
なお、Goの内部実装についてはネットを探すといくつか情報があります。 さらに詳しいことは、これらの情報を参照してください。
- Koki Ideさん「Goのスケジューラー実装とハマりポイント」
- POVILAS VERSOCKAS「GO SCHEDULER: MS, PS & GS」
- Brandon Gao「Go Runtime Scheduler」
runtimeパッケージのgoroutine関連の機能
軽量スレッドであるgoroutineを使うには、前回説明したように、goを付けて関数呼び出しを行うだけです。 しかし、場合によっては、ベースとなるOSのスレッドに対して何らかの制限を課すといった、より低レベルの操作をしたいこともあります。 runtimeパッケージには、そのようなときに使える低レベルの関数がいくつかあります。
なお、runtimeパッケージにはgoroutineのプロファイリング用の関数もいくつかありますが、集計機能などを自分で実装する必要があります。 goroutineのプロファイルを行うときは、ドキュメントにあるように、runtime/pprofなどの既成のパッケージを利用すべきです。
runtime.LockOSThread()
/ runtime.UnlockOSThread()
runtime.LockOSThread()
を呼ぶことで、現在実行中のOSスレッドでのみgoroutineが実行されるように束縛できます。 さらに、そのスレッドが他のgoroutineによって使用されなくなります。 これらの束縛は、runtime.UnlockOSThread()
を呼んだり、ロックしたgoroutineが終了すると解除されます。
この機能が必要になる状況としては、メインスレッドでの実行が強制されているライブラリ(GUIのフレームワークや、OpenGLとその依存ライブラリなど)をGo言語で利用する場合が挙げられます。 「現在実行中のスレッド」が確実にメインスレッドかどうかは、実行中にはわかりませんが、mainパッケージのinit()
関数は確実にメインスレッドで実行されるため、それを利用してメインスレッドを固定することができます1。
Goのランタイムでは、シグナルを受け取るスレッドを固定するためにこれらの関数を使っています。
runtime.Gosched()
現在実行中のgoroutineを一時中断して、他のgoroutineに処理を回します。 goroutineには、OSスレッドとは異なり、タスクをスリープ状態にしたり復元したりする機能がありません。ランキューの順番が回ってきたら、何ごともなく処理が再開します。
この関数は、ロックを使わずに目的の変数が変更されるのを待つといった用途で使えるかもしれませんが、実際にはあまり使うこともないでしょう。 Go言語のソースコードを見ても、ガベージコレクタが自発的に処理を手放すことでGCの停止時間を短縮させるのに使っているぐらいです。
runtime.GOMAXPROCS(n)
/ runtime.NumCPU()
ウェブに残る古いGo言語の解説記事では、このruntime.GOMAXPROCS()
をよく見かけると思います。 これは、同時に実行するOSスレッド数(I/Oのブロック中のスレッドは除く)を制御する関数です。 Go 1.4までは、デフォルトのPROC数が1となっており、マルチコアを活用するには必ずこの関数を呼ぶ必要がありました。 runtim.NumCPU()
でCPU数が分かるので、これをruntime.GOMAXPROCS
に渡すのが定石でした。
Go 1.5からは、デフォルトでruntime.GOMAXPROCS()
が設定されるようになったので、特別な場合を除いてわざわざ設定する必要はありません。 しかし、最速を狙おうとすると、このデフォルト値の半分に設定するほうがスループットが上がる場合があります。 現代のCPUのいくつかは、余剰のCPUリソースを使って1コアで2つ以上のスレッドを同時に実行する機構(ハイパースレッディングや、SMT(Simultaneous Multi-Threading))を備えています。 そのような機構を利用している場合、1コアで2つのヘビーな計算を同時に実行すると、CPUコアのリソースを食い合ってパフォーマンスが上がらないことがあります2。 筆者が執筆で使っているMacBookPro(Core i7の2014モデル)の場合、runtime.NumCPU()
は8を返しますが、これも物理コア数が4で、SMT機能による論理コア数がこの返り値となっています。
次の表は、実際にGo言語でCPUヘビーな計算を並列に行わせて(それぞれのgoroutineでは同じ計算をしている)、完了にかかる時間を計測した結果です。 1から4とくらべて、8並列の場合のパフォーマンスが落ちていることが分かります。
並列数 | 時間(秒) |
---|---|
1 | 11.834 |
2 | 11.971 |
4 | 12.294 |
8 | 15.181 |
Go言語のruntime.NumCPU()
はSMTを含めた論理コア数を返しますが、物理コア数を返すAPIはありませんので、プログラム側から自動設定はできません。 GOMAXPROCS
環境変数によっても設定できるので、実行時にこの環境変数で設定するほうがよいでしょう。 また、goroutineを特定のコアに張り付けてリソースの食い合いが起きないように制御する機能は提供されていませんが、現代のOSのスケジューラは十分に賢いので、4並列で重い計算を行う場合は自動で別のコアに分散して計算してくれます。
Race Detector
Go言語には、データ競合を発見する機能があります3。 この機能は、Race Detectorと呼ばれ、go build
やgo run
コマンドに-race
オプションを追加するだけで使えます。
Race Detectorを有効にしてGoプログラムを実行すると、次のようなメッセージが表示され、 競合が発生した個所と、競合した書き込みを行ったgoroutine、そのgoroutineの生成場所が分かります。
==================
WARNING: DATA RACE
Read at 0x0000011a7118 by goroutine 7:
main.main.func1()
/Users/shibu/.../mutex2.go:25 +0x41
Previous write at 0x0000011a7118 by goroutine 6:
main.main.func1()
/Users/shibu/.../mutex2.go:25 +0x60
Goroutine 7 (running) created at:
main.main()
/Users/shibu/.../mutex2.go:26 +0x93
Goroutine 6 (finished) created at:
main.main()
/Users/shibu/.../mutex2.go:26 +0x93
==================
syncパッケージ
前回の記事で紹介したように、チャネルとselect
の2つの文法があればgoroutine間の同期には事足ります。 しかし、すでに他の言語で書かれている実績あるコードをGo言語で再実装する場合など、他言語にはないチャネルとselect
で書き直すのは大変です。 そのようなときのために、並列処理をサポートするためのsyncパッケージが提供されています。
syncパッケージは、おそらく既存のAPIを参考にして実装されており、 多くのOSなどで並行・並列処理を同期させるための機能として提供されているものがいくつか含まれています。 参考までに、多くのOSでサポートされているPThreadのAPIとsyncパッケージの機能を比較してみます。
機能 | ネイティブAPI | syncパッケージ(Go言語) |
---|---|---|
ロック | pthread_mutex_* |
sync.Mutex |
RWロック | pthread_rwlock_* |
sync.RWMutex |
複数スレッドの終了待ち | なし | sync.WaitGroup |
条件変数 | pthread_cond_* |
sync.Cond |
一度だけ実行 | pthread_once |
sync.Once |
スレッドローカルストレージ | pthread_key_* など |
なし |
スピンロック | pthread_spin_* |
なし |
オブジェクトプール | なし | sync.Pool |
上記の表を見ると、Goのsyncパッケージに欠けている機能がいくつかあります。
スレッドローカルストレージは、スレッドごとの領域にデータを保存できる機能ですが、利用するにはスレッドを識別できる必要があります。 goroutineはOSのスレッドとは異なり、IDを持たないため、スレッドローカルストレージが使えません。
スピンロックは、ビジーループでロックを獲得するロックです。 goroutineの軽量スレッドは、ブロックしてしまってもコンテキストスイッチが軽いため、スピンロックの重要性があまりなく、この機能が提供されていません。
なお、ロックとRWロックは機能として提供されていますが、ブロックせずにトライできないか試してみるpthread_mutex_try_lock()
や、タイムアウト時間の設定ができるpthread_mutex_timedlock()
は存在しません。
sync.Mutex
/ sync.RWMutex
マルチスレッドプログラミングでは、「メモリ保護のためにロックを使う」といった説明をされることがあります。 これはスレッドが同じメモリ空間で動くためですが、実際に保護するのは実行パスであり、メモリを直接保護するわけではありません。 sync.Mutex
は、実行パスに入ることが可能なgoroutineを、排他制御によって制限するのに使います。
sync.Mutex
を使うと、「メモリを読み込んで書き換える」コードに入るgoroutineが1つに制限されるため、不整合を防ぐことができます。 この、同時に実行されると問題がおきる実行コードの行(1行とは限らないが、プログラミング言語のブロックより小さいこともある)を、クリティカルセクションと呼びます。 マップや配列に対する操作はアトミックではないため、複数のgoroutineからアクセスする場合には保護が必要です。
次のコードでは、IDをインクリメントするコードが同時に1つしか実行されないようにしています。 マルチスレッドプログラミングでは「コードのどの箇所でコンテキストスイッチが発生しても不整合が置きないようにする」のが鉄則です。 id
変数をCPUが読み込んだタイミングでコンテキストスイッチが発生すると、同じid
値を参照するスレッドが複数できてしまい、同じid
が生成されてしまいます。
package main
import (
"fmt"
"sync"
)
var id int
func generateId(mutex *sync.Mutex) int {
// Lock()/Unlock()をペアで呼び出してロックする
mutex.Lock()
id++
mutex.Unlock()
return id
}
func main() {
// sync.Mutex構造体の変数宣言
// 次の宣言をしてもポインタ型になるだけで正常に動作します
// mutex := new(sync.Mutex)
var mutex sync.Mutex
for i := 0; i < 100; i++ {
go func() {
fmt.Printf("id: %d\n", generateId(&mutex))
}()
}
}
sync.Mutex
は内部に整数を2つ持つ構造体です。 Go言語では、構造体作成時にはかならずメモリが初期化されるため、上記の例のように特別な初期化を行わずに使えます。 ただし、値コピーしてしまうとロックしている状態のまま別のsync.Mutex
インスタンスになってしまうため、他の関数に渡すときは必ずポインタで渡すようにします。コードの静的チェックツールのgo vet
を実行すると、このsync.Mutex
のコピー問題は発見できます。
Go言語には、そのコードブロックを抜けるときに忘れずに後処理を行うdefer
があるので、これと組み合わせて使うのがほとんどでしょう。 メモリ確保と解放、ファイルのオープンとクローズ、ロックの獲得と解放などの対となる動作はなるべく不可分に記述する、連続して書くというのが、間違いを防ぐためのベストプラクティスとして多くの言語で採用されています4。
func generateId(mutex *sync.Mutex) int {
// 多くの場合は次のように連続して書く
mutex.Lock()
defer mutex.Unlock()
id++
return id
}
Go言語Wikiには、Mutexとチャネルの使い分けについて次のようにまとめられています5。
- チャネルが有用な用途:データの所有権を渡す場合、作業を並列化して分散する場合、非同期で結果を受け取る場合
- Mutexが有用な用途:キャッシュ、状態管理
sync.Mutex
にはsync.RWMutex
というバリエーションがあります。 この構造体にはsync.Mutex
と同じLock()
、Unlock()
に加えて、RLock()
、RUnlock
というメソッドがあります。 R
が付くほうは、読み込み用のロックの取得と解放で、 「読み込みはいくつものgoroutineが並列して行えるが、書き込み時には他のgoroutineの実行を許さない」という方式でのロックが行えます。 Mutexの用途のうち、読み込みと書き込みがほぼ同時に行われるような状態管理の場合はsync.Mutex
が、複数のgoroutineで共有されるキャッシュの保護にはsync.RWMutex
が適しています。
なお、上記のコードにはバグがあります。 このバグを直す方法はいくつかありますが、一番簡単なのが、次節で説明するsync.WaitGroup
を使う方法です。
sync.WaitGroup
sync.Mutex
並に使用頻度が高いのがsync.WaitGroup
です。 sync.WaitGroup
は、多数のgoroutineで実行しているジョブの終了待ちに使います。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// ジョブ数をあらかじめ登録
wg.Add(2)
go func() {
// 非同期で仕事をする(1)
fmt.Println("仕事1")
// Doneで完了を通知
wg.Done()
}()
go func() {
// 非同期で仕事をする(1)
fmt.Println("仕事2")
// Doneで完了を通知
wg.Done()
}()
// すべての処理が終わるのを待つ
wg.Wait()
fmt.Println("終了")
}
Add()
メソッドを呼ぶとジョブ数が追加されます。 Add()
メソッドは、必ずgoroutineなどを作成する前に呼びましょう。
Done()
メソッドを呼ぶと残りジョブ数がデクリメントされていきます。 Wait()
メソッドはすべてのジョブが完了するのを待つメソッドです。
チャネルよりもsync.WaitGroup
のほうがよいのは、ジョブ数が大量にあったり、可変個だったりする場合です。 100以上のgoroutineのためにチャネルを大量に作成して終了状態を伝達することもできますが、これだけ大量のジョブであれば、数値のカウントだけでスケールするsync.WaitGroup
のほうがリーズナブルです。
前節のサンプルコードは、終了待ちをしていないため、main()
が終了したらプログラムが完了してしまいます。 sync.WaitGroup
構造体を使えば、簡単に全タスクの終了待ちができます。
sync.WaitGroup
も変数宣言だけで使えます。 値コピーしてしまうと正しく動作しない点もsync.Mutex
と同じです。
sync.Once
sync.Once
は、一度だけ関数を実行したいときに使います。 初期化処理を一度だけ行いたいときに使う場合が多いでしょう。
package main
import (
"fmt"
"sync"
)
func initialize() {
fmt.Println("初期化処理")
}
var once sync.Once
func main() {
// 三回呼び出しても一度しか呼ばれない。
once.Do(initialize)
once.Do(initialize)
once.Do(initialize)
}
Go言語には、init()
という名前の関数がパッケージ内にあると、それが初期化関数として呼ばれる機能があります6。 sync.Once
ではなくinit()
を使うほうが、初期化処理を呼び出すコードを書かなくても実行され、コード行数も減るので、シンプルです。 sync.Once
をあえて使うのは、初期化処理を必要なときまで遅延させたい場合でしょう。
sync.Cond
sync.Cond
は条件変数と呼ばれる排他制御の仕組みです。 これもロックをかけたり解除したりすることでクリティカルセクションを保護します。 sync.Cond
の用途には次の2つがあります。
- 先に終わらせなければいけないタスクがあり、それが完了したら待っているすべてのgoroutineに通知する(
Broadcast()
メソッド) - リソースの準備ができ次第、そのリソースを待っているgoroutineに通知をする(
Signal()
メソッド)
後者の用途の場合、Goであればチャネルで用が済むので、主に使うことになるのは前者の用途でしょう。 Goの標準ライブラリでは、TLSやHTTP/2のライブラリがサーバーとのハンドシェイクが完了したり、サーバーからレスポンスがきたタイミングでワーカースレッドを起こすのに、sync.Cond
が使われています。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mutex sync.Mutex
sync.NewCond(&mutex)
for _, name := range []string{"A", "B", "C"} {
go func(name string) {
// ロックしてからWaitメソッドを呼ぶ
mutex.Lock()
defer mutex.Unlock()
// Broadcast()が呼ばれるまで待つ
cond.Wait()
// 呼ばれた!
fmt.Println(name)
}(name)
}
fmt.Println("よーい")
time.Sleep(time.Second)
fmt.Println("どん!")
// 待っているgoroutineを一斉に起こす
cond.Broadcast()
time.Sleep(time.Second)
}
チャネルの場合、待っているすべてのgoroutineに通知するとしたらクローズするしかないため、一度きりの通知にしか使えません。 sync.Cond
であれば、何度でも使えます。また、通知を受け取るgoroutineの数がゼロであっても複数であっても同じように扱えます。
sync.Pool
sync.Pool
は、オブジェクトのキャッシュを実現する構造体です。 一時的な状態を保持する構造体をプールしておいて、goroutine間でシェアできます。 sync.Pool
は当然ながらgoroutineセーフです。この構造体は、fmt
パッケージの一時的な内部出力バッファなどを保持する目的で導入されました。
sync.Pool
の使い方は簡単で、Put()
メソッドでキャッシュしたいデータを追加します。 Get()
で、キャッシュからデータを取り出します。 出し入れするデータはinterface{}
型なので、どのような要素でも入れられます。プールが空のときは、プール作成時に設定した関数で作ったインスタンスが返ります。
package main
import (
"fmt"
"sync"
)
func main() {
// Poolを作成。Newで新規作成時のコードを実装
var count int
pool := sync.Pool{
New: func() interface{} {
count++
return fmt.Sprintf("created: %d", count)
},
}
// 追加した要素から受け取れる
// プールが空だと新規作成
pool.Put("manualy added: 1")
pool.Put("manualy added: 2")
fmt.Println(pool.Get())
fmt.Println(pool.Get())
fmt.Println(pool.Get()) // これは新規作成
}
なお、内部では要素はキャッシュでしかなく、(他の言語で言うところの)WeakRefのコンテナとなっています。 そのため、ガベージコレクタが稼働すると、保持しているデータが削除されます。 sync.Pool
は、消えては困る重要なデータのコンテナには適しません。
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var count int
pool := sync.Pool{
New: func() interface{} {
count++
return fmt.Sprintf("created: %d", count)
},
}
// GCを呼ぶと追加された要素が消える
pool.Put("removed 1")
pool.Put("removed 2")
runtime.GC()
fmt.Println(pool.Get())
}
sync/atomicパッケージ
sync/atomicパッケージは不可分操作7と呼ばれる操作を提供しています。
これはCPUレベルで提供されている「1つで複数の操作を同時に行う命令」などを駆使したり、提供されていなければ正しく処理が行われるまでループするという命令を駆使して「確実に実行される」ことを保証している関数として提供されています。 途中でコンテキストスイッチが入って操作が失敗しないことが保証されます。
次の表のように6つのデータ型に対して、5つの操作が提供されています。
データ型 | 加算 | 比較してスワップ | 変数から読み込み | 変数に書き込み | スワップ |
---|---|---|---|---|---|
int32 |
AddInt32 |
CompareAndSwapInt32 |
LoadInt32 |
StoreInt32 |
SwapInt32 |
int64 |
AddInt64 |
CompareAndSwapInt64 |
LoadInt64 |
StoreInt64 |
SwapInt64 |
uint32 |
AddUint32 |
CompareAndSwapUint32 |
LoadUint32 |
StoreUint32 |
SwapUint32 |
uint64 |
AddUint64 |
CompareAndSwapUint64 |
LoadUint64 |
StoreUint64 |
SwapUint64 |
uintptr |
AddUintptr |
CompareAndSwapUintptr |
LoadUintptr |
StoreUintptr |
SwapUintptr |
unsafe.Pointer |
CompareAndSwapPointer |
LoadPointer |
StorePointer |
SwapPointer |
最初のsync.Mutex
のサンプルをatomic
を使って表現すると次のようになります。
var id int64
func generateId(mutex *sync.Mutex) int {
return atomic.AddInt64(&id, 1)
}
sync.Mutex
やsync.Cond
、チャネルなどを使っても、複数のgoroutineがアクセスしてロックされると、コンテキストスイッチが発生します。 こちらのロックフリーな関数を使えばコンテキストスイッチが発生しないため、うまく用途が合えば今回紹介した機能の中では最速です。
これ以外に、任意のデータ型(interface{}
)に対するLoad()
メソッドとStore()
メソッドによる、アトミックな変数読み書きを提供するatomic.Value
構造体もあります。
まとめと次回予告
スレッドやgoroutineの仕組みと、Goの並行・並列処理をサポートするツールやライブラリを見てきました。 前回紹介したように、Go言語は組み込みで「安全に非同期な待ち合わせをする機能」を提供しています。 しかし、Go言語の非同期サポートの強力さを実感するのは、goroutineがデッドロックしたときに稼働中のgoroutineがどこでブロックしているかを教えてくれたり、競合状態を検出するオプションがあったり、パニック時にきちんとスタックトレースが出てくれる点です。 こうした強力な機能のおかげで、Go言語ではマルチスレッドを駆使したコードのデバッグが極めて簡単です。
次回は並列処理のパターンを取り上げます。
脚注
- https://github.com/golang/go/wiki/LockOSThread↩
- 最近、AMDの最新CPUであるRYZENのベンチーマークでも、この問題が話題になりました。「4gamer:Ryzenはなぜ「ゲーム性能だけあと一歩」なのか? テストとAMD担当者インタビューからその特性と将来性を本気で考える」: http://www.4gamer.net/games/300/G030061/20170425122/↩
- https://golang.org/doc/articles/race_detector.html↩
- C++のスマートポインタとデストラクタ、例外処理を持つ言語のtry/finallyブロック、Rubyのブロック構文やPythonのwith構文など、身の回りでたくさん発見できるでしょう。↩
- https://github.com/golang/go/wiki/MutexOrChannel↩
- http://qiita.com/tenntenn/items/7c70e3451ac783999b4f↩
- https://ja.wikipedia.org/wiki/%E4%B8%8D%E5%8F%AF%E5%88%86%E6%93%8D%E4%BD%9C↩

この連載の記事
-
第20回
プログラミング+
Go言語とコンテナ -
第19回
プログラミング+
Go言語のメモリ管理 -
第18回
プログラミング+
Go言語と並列処理(3) -
第16回
プログラミング+
Go言語と並列処理 -
第15回
プログラミング+
Go言語で知るプロセス(3) -
第14回
プログラミング+
Go言語で知るプロセス(2) -
第13回
プログラミング+
Go言語で知るプロセス(1) -
第12回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(3) -
第11回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(2) -
第10回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(1) - この連載の一覧へ