syncパッケージ
レッスン16:syncパッケージ — 並行処理の安全性の守護者
例え話
公衆トイレを想像してください:
- Mutex(相互排除ロック):ドアに鍵がかかっている;1人が入ると鍠を閉め、他の人は待つ必要があります。同時に1人しか使用できません。
- RWMutex(読み書きロック):図書館の読書室——多くの人が同時に読書できますが(読み取りロック)、誰かが資料を変更したい場合は、すべての読者が退出するまで待ち、排他的アクセスが必要です(書き込みロック)。
- Once(単一実行):会社に入社する際、社員証は1回だけ発行されます。何度お願いしても、初回以降は繰り返されません。
- Pool(オブジェクトプール):会議室の椅子——使用後に倉庫に戻し、次の会議でまた取り出します。毎回新しいものを購入するのではなく。
- sync.Map(並行処理安全なマップ):複数の鍵を持つ安全な郵便受け;複数の郵便配達員が異なる区画に同時に配達でき、お互いに干渉しません。
コアコンセプト
1. なぜsyncパッケージが必要なのでしょうか?
Goのgoroutineは軽量で強力ですが、複数のgoroutineが同時に共有データにアクセスすると、競合状態が発生します:
GO
// 危険!複数のgoroutineが同時にcountを変更
var count int
for i := 0; i < 1000; i++ {
go func() {
count++ // データレース!
}()
}
syncパッケージは並行処理の安全性を確保するための同期プリミティブを提供します。
2. コアタイプの概要
| タイプ | 目的 | 特徴 |
|---|---|---|
sync.Mutex |
相互排除ロック | 同時に1つのgoroutineのみが保持可能 |
sync.RWMutex |
読み書きロック | 複数の読み取り、単一の書き込み;読み取りは競合しない |
sync.WaitGroup |
待機グループ | グループのgoroutineの完了を待機(レッスン15で説明済み) |
sync.Once |
単一実行 | 関数が1回だけ実行されることを保証 |
sync.Pool |
オブジェクトプール | オブジェクトを再利用し、メモリ割り当てを削減 |
sync.Map |
並行処理安全なマップ | ロックなしの並行キーバリューストレージ |
sync.Cond |
条件変数 | goroutine間の条件通知 |
atomicパッケージ |
アトミック操作 | 最も低レベルで効率的な並行処理安全な操作 |
基本構文と使い方
💡 Mutex
GO
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
count := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // ロック
count++ // 共有変数を安全に変更
mu.Unlock() // アンロック
}()
}
wg.Wait()
fmt.Println("count =", count) // 出力:count = 1000
}
💡 ヒント:
defer mu.Unlock()を使用すると、関数がパニックを起こしてもロックが解放され、デッドロックを防ぎます。
💡 RWMutex(読み書きロック)
GO
package main
import (
"fmt"
"sync"
)
func main() {
var rwmu sync.RWMutex
data := make(map[string]string)
var wg sync.WaitGroup
// 書き込み操作:書き込みロックを使用
wg.Add(1)
go func() {
defer wg.Done()
rwmu.Lock() // 書き込みロック:排他的
data["key"] = "value"
rwmu.Unlock()
}()
// 読み取り操作:読み取りロックを使用(並列実行可能)
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
rwmu.RLock() // 読み取りロック:共有
_ = data["key"]
rwmu.RUnlock()
}()
}
wg.Wait()
fmt.Println("data:", data)
}
💡 ヒント: 読み取り頻度が高く書き込み頻度が低いシナリオでは
RWMutexが優れたパフォーマンスを発揮します。読み取りと書き込みの頻度が同程度の場合はMutexの方がシンプルです。
💡 Once(単一実行)
GO
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
var wg sync.WaitGroup
setup := func() {
fmt.Println("初期化(1回だけ実行されます)")
}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
once.Do(setup) // 最初のgoroutineのみがsetupを実行
fmt.Printf("goroutine %d が完了しました\n", id)
}(i)
}
wg.Wait()
}
💡 ヒント:
Onceは複数のgoroutineが同時に呼び出しても、提供された関数が1回だけ実行されることを保証します。シングルトン初期化によく使用されます。
💡 Pool(オブジェクトプール)
GO
package main
import (
"fmt"
"sync"
)
func main() {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("新しいオブジェクトを作成")
return make([]byte, 1024)
},
}
// 最初のGet:Newを呼び出して作成
buf1 := pool.Get().([]byte)
fmt.Println("オブジェクトを取得、長さ:", len(buf1))
// 使用後に返却
pool.Put(buf1)
// 2回目のGet:前に返却されたオブジェクトを再利用
buf2 := pool.Get().([]byte)
fmt.Println("オブジェクトを再利用、長さ:", len(buf2))
}
💡 ヒント:
Pool内のオブジェクトは任意のGCサイクル中にガベージコレクションされる可能性があります。Putしたオブジェクトが常にGetできるとは限らないので、長期的なストレージとして使用しないでください。
💡 アトミック操作
GO
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var count int64 = 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&count, 1) // アトミックインクリメント
}()
}
wg.Wait()
fmt.Println("count =", atomic.LoadInt64(&count)) // アトミック読み取り
}
💡 ヒント:
atomic操作はMutexよりも軽量で、シンプルなカウンターやフラグなどに適しています。
💡 レース検出
BASH
go run -race main.go # 実行時にレースを検出
go test -race ./... # テスト中にレースを検出
💡 ヒント: 開発中は常に
-raceを有効にすることをお勧めします。隠れたデータレースの問題を発見するのに役立ちます。
実践的な例
例:スレッドセーフなカウンター(難易度⭐)
ミューテックスでカプセル化された安全なカウンター:
GO
package main
import (
"fmt"
"sync"
)
// SafeCounterはスレッドセーフなカウンターです
type SafeCounter struct {
mu sync.Mutex
count int
}
// Incはカウンターをインクリメントします
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
// Valueは現在の値を返します
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := &SafeCounter{}
var wg sync.WaitGroup
// 100個のgoroutineを起動し、各goroutineが100回インクリメント
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
counter.Inc()
}
}()
}
wg.Wait()
fmt.Println("最終カウント:", counter.Value()) // 出力:最終カウント:10000
}
例:sync.Mapによる並行処理安全なキャッシュ(難易度⭐⭐)
GO
package main
import (
"fmt"
"sync"
)
// Cacheは並行処理安全なキャッシュです
type Cache struct {
store sync.Map
}
// Setはキャッシュに値を格納します
func (c *Cache) Set(key string, value interface{}) {
c.store.Store(key, value)
}
// Getはキャッシュから値を取得します
func (c *Cache) Get(key string) (interface{}, bool) {
c.store.Load(key)
}
// Deleteはキャッシュからキーを削除します
func (c *Cache) Delete(key string) {
c.store.Delete(key)
}
// GetOrSetはアトミックに値を取得または設定します(冗長な計算を回避)
func (c *Cache) GetOrSet(key string, factory func() interface{}) interface{} {
if val, ok := c.store.Load(key); ok {
return val
}
// LoadOrStoreを使用してアトミック性を確保
val, _ := c.store.LoadOrStore(key, factory())
return val
}
func main() {
cache := &Cache{}
var wg sync.WaitGroup
// 並列書き込み
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key-%d", id)
cache.Set(key, id*10)
fmt.Printf("書き込み:%s = %d\n", key, id*10)
}(i)
}
wg.Wait()
// 並列読み取り
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key-%d", id)
if val, ok := cache.Get(key); ok {
fmt.Printf("読み取り:%s = %v\n", key, val)
}
}(i)
}
wg.Wait()
// GetOrSetを使用して冗長な計算を回避
result := cache.GetOrSet("computed", func() interface{} {
fmt.Println("複雑な計算を実行中...")
return 42
})
fmt.Println("computed =", result)
// 再度取得、再計算されない
result2 := cache.GetOrSet("computed", func() interface{} {
fmt.Println("この行は実行されません")
return 99
})
fmt.Println("computed =", result2)
}
例:タイムアウト付きワーカープール(syncの総合使用)(難易度⭐⭐⭐)
GO
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
)
// Taskはタスクを表します
type Task struct {
ID int
Data string
}
// Resultは結果を表します
type Result struct {
TaskID int
Output string
WorkerID int
Duration time.Duration
}
// WorkerPoolはワーカープールです
type WorkerPool struct {
workerCount int
taskCh chan Task
resultCh chan Result
wg sync.WaitGroup
processed int64 // アトミックカウンター
errors int64
once sync.Once // 単一の初期化を保証
pool sync.Pool // Resultオブジェクトを再利用
}
// NewWorkerPoolはワーカープールを作成します
func NewWorkerPool(workerCount, taskBufferSize int) *WorkerPool {
wp := &WorkerPool{
workerCount: workerCount,
taskCh: make(chan Task, taskBufferSize),
resultCh: make(chan Result, taskBufferSize),
}
// オブジェクトプールを初期化
wp.pool = sync.Pool{
New: func() interface{} {
return &Result{}
},
}
return wp
}
// Startはワーカープールを開始します(1回だけ実行)
func (wp *WorkerPool) Start(ctx context.Context) {
wp.once.Do(func() {
for i := 0; i < wp.workerCount; i++ {
wp.wg.Add(1)
go wp.worker(ctx, i)
}
fmt.Printf("ワーカープールを%d個のワーカーで開始しました\n", wp.workerCount)
})
}
// workerはワーカーgoroutineです
func (wp *WorkerPool) worker(ctx context.Context, id int) {
defer wp.wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("ワーカー %d:終了シグナルを受信\n", id)
return
case task, ok := <-wp.taskCh:
if !ok {
fmt.Printf("ワーカー %d:タスクチャネルが閉じられました\n", id)
return
}
// プールからResultを取得
result := wp.pool.Get().(*Result)
result.TaskID = task.ID
result.WorkerID = id
// タスク処理をシミュレート
start := time.Now()
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
result.Output = fmt.Sprintf("処理済み:%s", task.Data)
result.Duration = time.Since(start)
atomic.AddInt64(&wp.processed, 1)
wp.resultCh <- *result
// Resultオブジェクトをプールに返却(注意:値コピーを送信)
wp.pool.Put(result)
}
}
}
// Submitはタスクを提出します
func (wp *WorkerPool) Submit(task Task) {
wp.taskCh <- task
}
// Closeはワーカープールを閉じます
func (wp *WorkerPool) Close() {
close(wp.taskCh)
wp.wg.Wait()
close(wp.resultCh)
}
// Statsは統計を返します
func (wp *WorkerPool) Stats() (processed, errors int64) {
return atomic.LoadInt64(&wp.processed), atomic.LoadInt64(&wp.errors)
}
func main() {
rand.Seed(time.Now().UnixNano())
// タイムアウト付きコンテキストを作成
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// ワーカープールを作成
pool := NewWorkerPool(3, 20)
pool.Start(ctx)
// タスクを提出
var submitWg sync.WaitGroup
submitWg.Add(1)
go func() {
defer submitWg.Done()
for i := 1; i <= 10; i++ {
pool.Submit(Task{
ID: i,
Data: fmt.Sprintf("task-data-%d", i),
})
fmt.Printf("タスク #%d を提出しました\n", i)
}
}()
// 結果を収集
var collectWg sync.WaitGroup
collectWg.Add(1)
go func() {
defer collectWg.Done()
for result := range pool.resultCh {
fmt.Printf("タスク#%d はワーカー%d によって処理、所要時間 %v -> %s\n",
result.TaskID, result.WorkerID, result.Duration, result.Output)
}
}()
// タスク提出の完了を待機
submitWg.Wait()
// ワーカープールを閉じる
pool.Close()
// 結果収集の完了を待機
collectWg.Wait()
// 統計を出力
processed, _ := pool.Stats()
fmt.Printf("\n統計:合計 %d 個のタスクを処理しました\n", processed)
}
実践的な使用例
ケース1:並列HTTPリクエストのレート制限
クローラーやAPI呼び出しでは、BANされるのを避けるために並行数を制限する必要があります:
GO
package main
import (
"fmt"
"sync"
"time"
)
// RateLimiterはシンプルな並行レートリミッターです
type RateLimiter struct {
sem chan struct{}
mu sync.Mutex
active int
maxConc int
}
// NewRateLimiterはレートリミッターを作成します;maxConcは最大同時実行数です
func NewRateLimiter(maxConc int) *RateLimiter {
return &RateLimiter{
sem: make(chan struct{}, maxConc),
maxConc: maxConc,
}
}
// Acquireは許可を取得します
func (r *RateLimiter) Acquire() {
r.sem <- struct{}{}
r.mu.Lock()
r.active++
r.mu.Unlock()
}
// Releaseは許可を解放します
func (r *RateLimiter) Release() {
<-r.sem
r.mu.Lock()
r.active--
r.mu.Unlock()
}
// ActiveCountは現在のアクティブ数を返します
func (r *RateLimiter) ActiveCount() int {
r.mu.Lock()
defer r.mu.Unlock()
return r.active
}
func main() {
limiter := NewRateLimiter(3) // 最大同時実行数3
var wg sync.WaitGroup
urls := []string{
"https://api.example.com/page1",
"https://api.example.com/page2",
"https://api.example.com/page3",
"https://api.example.com/page4",
"https://api.example.com/page5",
"https://api.example.com/page6",
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
limiter.Acquire() // 許可を取得(3つ以上の場合はブロック)
defer limiter.Release() // 許可を解放
fmt.Printf("[同時実行数:%d] リクエスト開始:%s\n", limiter.ActiveCount(), u)
time.Sleep(time.Duration(100+len(u)*10) * time.Millisecond) // リクエストをシミュレート
fmt.Printf("[同時実行数:%d] リクエスト完了:%s\n", limiter.ActiveCount(), u)
}(url)
}
wg.Wait()
fmt.Println("すべてのリクエストが完了しました")
}
ケース2:遅延設定読み込み(シングルトンパターン)
sync.Onceを使用して設定が1回だけ読み込まれることを保証します:
GO
package main
import (
"fmt"
"sync"
)
// Configはアプリケーション設定です
type Config struct {
DatabaseURL string
APIKey string
MaxRetries int
}
var (
config *Config
once sync.Once
)
// GetConfigは設定を取得します(遅延読み込み、1回だけ初期化)
func GetConfig() *Config {
once.Do(func() {
fmt.Println("設定を読み込み中(1回だけ実行されます)...")
// ファイルや環境変数からの設定読み込みをシミュレート
config = &Config{
DatabaseURL: "postgres://localhost:5432/mydb",
APIKey: "sk-xxxxxxxxxxxx",
MaxRetries: 3,
}
})
return config
}
func main() {
var wg sync.WaitGroup
// 複数のgoroutineが同時に設定を取得
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cfg := GetConfig()
fmt.Printf("goroutine %d:DB=%s、リトライ=%d\n",
id, cfg.DatabaseURL, cfg.MaxRetries)
}(i)
}
wg.Wait()
fmt.Println("設定の読み込みが完了しました")
}
❓ よくある質問
Q1:MutexとRWMutexのどちらを選択すべきですか?
| シナリオ | 推奨 |
|---|---|
| 読み取り頻度が高く書き込み頻度が低い(例:キャッシュ) | RWMutex — 複数の読み取りを並列実行可能 |
| 読み書きのバランスが取れている、または書き込み頻度が高い | Mutex — シンプルでオーバーヘッドが少ない |
| シンプルなカウンター | atomic — 最も軽量 |
GO
// ❌ 読み取り頻度が高いシナリオでMutexを使用——パフォーマンスが低下
var mu sync.Mutex
mu.Lock()
val := cache[key] // 読み取り操作も排他的
mu.Unlock()
// ✅ RWMutexを使用——読み取り操作は並列実行可能
var rwmu sync.RWMutex
rwmu.RLock()
val := cache[key] // 複数のgoroutineが同時に読み取り可能
rwmu.RUnlock()
Q2:sync.Mapとロック付きマップのどちらを選択すべきですか?
GO
// シナリオ1:キーバリューペアが比較的静的、読み取り頻度が高く書き込み頻度が低い -> sync.Map
var m sync.Map
m.Store("key", "value")
val, _ := m.Load("key")
// シナリオ2:頻繁な追加/削除、イテレーションが必要 -> ロック付きマップ
type SafeMap struct {
mu sync.RWMutex
m map[string]string
}
// sync.Mapは以下に適しています:
// 1. キーが1回だけ書き込まれ、何度も読み取られる(例:キャッシュ)
// 2. 複数のgoroutineが異なるキーを読み書きする(重複なし)
Q3:Pool内のオブジェクトはいつガベージコレクションされますか?
Pool内のオブジェクトは任意のGCサイクル中にクリアされる可能性があります。Poolを長期的なストレージとして使用しないでください:
GO
// ❌ 誤った使用法:Poolをキャッシュとして使用
pool := &sync.Pool{New: func() interface{} { return expensiveObject() }}
obj := pool.Get()
// ... 使用
pool.Put(obj)
// 次のGC後、objは収集されている可能性がある
// ✅ 正しい使用法:一時的なオブジェクトを再利用し、割り当てを削減
pool := &sync.Pool{
New: func() interface{} {
return make([]byte, 0, 4096) // バッファを事前割り当て
},
}
buf := pool.Get().([]byte)[:0] // 取得してリセット
// ... bufを使用
pool.Put(buf) // 返却
Q4:デッドロックを回避するには?
GO
// ❌ デッドロック:同じgoroutineが2回ロック
var mu sync.Mutex
mu.Lock()
mu.Lock() // 永久にブロック!デッドロック!
// ✅ 解決策1:deferを使用して解放を保証
mu.Lock()
defer mu.Unlock()
// ... パニック時にも解放される
// ✅ 解決策2:ロックの順序に注意し、クロスロックを回避
// 2つのgoroutine:1つはロックAを保持してBを待ち、もう1つはBを保持してAを待つ -> デッドロック
// 解決策:常にA->Bの順序でロック
// ✅ 解決策3:タイムアウト付きロックを使用(Go 1.18+ではcontextの使用を推奨)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// チャネルとselectでタイムアウト制御
📖 まとめ
| コンセプト | 重要なポイント |
|---|---|
sync.Mutex |
相互排除ロック、同時に1つのgoroutineのみが保持可能 |
sync.RWMutex |
読み書きロック、複数の読み取り、単一の書き込み;読み取り頻度が高いシナリオに最適 |
sync.Once |
関数が1回だけ実行されることを保証;シングルトン/初期化に適している |
sync.Pool |
オブジェクトプール、一時的なオブジェクトを再利用し、GC負荷を軽減 |
sync.Map |
並行処理安全なマップ;特定のシナリオでロック付きマップより優れている |
atomic |
アトミック操作;最も軽量な並行処理安全なソリューション |
-race |
レース検出器;開発中に必ず有効にすべき |
| デッドロック防止 | defer Unlock、一貫したロック順序、ネストされたロックの回避 |
選択原則:
- シンプルなカウンター/フラグ →
atomic - 一般的な相互排除 →
Mutex - 読み取り頻度が高く書き込み頻度が低い →
RWMutex - 単一の初期化 →
Once - オブジェクトの再利用 →
Pool - 並行キーバリューの読み書き →
sync.Map
📝 演習
演習1:スレッドセーフなスタックの実装
Push、Pop、Size操作をサポートする並行処理安全なスタック(LIFO)を実装してください。
GO
package main
import (
"errors"
"fmt"
"sync"
)
// ThreadSafeStackは並行処理安全なスタックです
type ThreadSafeStack struct {
// あなたのコード:
// - 適切なデータ構造を選択
// - 適切な同期プリミティブを選択
}
// NewStackは新しいスタックを作成します
func NewStack() *ThreadSafeStack {
// あなたのコード
return nil
}
// Pushはスタックに値をプッシュします
func (s *ThreadSafeStack) Push(val int) {
// あなたのコード
}
// Popはスタックから値をポップします(空の場合はエラーを返す)
func (s *ThreadSafeStack) Pop() (int, error) {
// あなたのコード
return 0, errors.New("スタックが空です")
}
// Sizeはスタックの要素数を返します
func (s *ThreadSafeStack) Size() int {
// あなたのコード
return 0
}
func main() {
stack := NewStack()
var wg sync.WaitGroup
// 並列プッシュ
for i := 0; i < 100; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
stack.Push(val)
}(i)
}
wg.Wait()
fmt.Println("スタックサイズ:", stack.Size()) // 100を出力すべき
// 並列ポップ
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if val, err := stack.Pop(); err == nil {
fmt.Println("ポップ:", val)
}
}()
}
wg.Wait()
fmt.Println("残りサイズ:", stack.Size()) // 50を出力すべき
}
演習2:TTL付きキャッシュの実装
sync.RWMutexを使用してTTL(生存時間)をサポートするキャッシュを実装してください:
GO
package main
import (
"fmt"
"sync"
"time"
)
// TTLCacheは有効期限付きキャッシュです
type TTLCache struct {
// あなたのコード:
// - ストレージ構造
// - 同期ロック
// - TTL設定
}
// NewTTLCacheはキャッシュを作成します;ttlは有効期限です
func NewTTLCache(ttl time.Duration) *TTLCache {
// あなたのコード
return nil
}
// Setはキーバリューペアを設定します
func (c *TTLCache) Set(key string, value interface{}) {
// あなたのコード
}
// Getは値を取得します(期限切れの場合はfalseを返す)
func (c *TTLCache) Get(key string) (interface{}, bool) {
// あなたのコード
return nil, false
}
// Deleteはキーを削除します
func (c *TTLCache) Delete(key string) {
// あなたのコード
}
// Sizeは有効なキーバリューペアの数を返します
func (c *TTLCache) Size() int {
// あなたのコード
return 0
}
func main() {
cache := NewTTLCache(2 * time.Second)
cache.Set("name", "Go言語")
cache.Set("version", "1.21")
if val, ok := cache.Get("name"); ok {
fmt.Println("name =", val)
}
fmt.Println("キャッシュサイズ:", cache.Size())
time.Sleep(3 * time.Second)
if _, ok := cache.Get("name"); !ok {
fmt.Println("nameは期限切れです")
}
fmt.Println("期限切れ後のサイズ:", cache.Size())
}
演習3:並列Map-Reduce
syncパッケージを使用してシンプルな並列Map-Reduceを実装してください:
GO
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// ParallelMapReduceはデータを並列に処理し、結果を集約します
// パラメータ:
// - data:入力データスライス
// - mapFn:マッピング関数、入力を値に変換
// - reduceFn:リダクション関数、2つの結果をマージ
//
// 要件:
// - データをセグメントに分割し、各セグメントをgoroutineで処理
// - sync.WaitGroupですべてのgoroutineの完了を待機
// - sync.Mutexでリダクション結果を保護
// - atomicで処理済み要素の総数をカウント
func ParallelMapReduce(
data []int,
mapFn func(int) int,
reduceFn func(int, int) int,
) int {
// あなたのコード:
// 1. セグメント数を決定(4セグメントを推奨)
// 2. 各セグメントを処理するgoroutineを起動
// 3. mapFnで各要素を処理
// 4. reduceFnでセグメント結果をマージ
// 5. 最終結果を返す
return 0
}
func main() {
data := make([]int, 100)
for i := range data {
data[i] = i + 1 // 1から100
}
// すべての要素の二乗の合計を計算
result := ParallelMapReduce(
data,
func(x int) int { return x * x }, // map:二乗
func(a, b int) int { return a + b }, // reduce:合計
)
fmt.Println("1² + 2² + 3² + ... + 100² =", result)
// 期待される出力:338350
}
次のレッスン
syncパッケージを完了したことで、Go並行処理プログラミングのコアツールをマスターしました。次に、総合的な演習を通じて学んだことを巩固します。



