Go言語

【Go言語】並行処理を書くときには「拘束」を意識すべきなんじゃ!!

拘束

情報(データ)を、確実に1つの並行プロセスからのみ得られるようにすること

これが実現できると

  • プログラマーがデータの中身を意識する負荷が下がる
  • クリティカルセクションが小さくなる
  • 並行プログラムが安全で、同期不要なものになる

拘束には次の2パターンある。

  • アドホック拘束
    • チーム内の規約(ルール)によって拘束する。例えば、「変数A は関数 B からしかアクセスしないようにしよう」とイメージ。
    • 規約を守り続けるのは難しい
  • レキシカル拘束
    • レキシカルスコープを使って適切な範囲でデータを公開する。

アドホック拘束

アドホック拘束のサンプルコード。

package main

import "fmt"

func main() {
	// dataはどこからでもアクセスできるが、loopData関数内のみからアクセスするように書かれている
	data := []int{10, 20, 30}
	// 送信専用チャネルを引数として受け取り
	// dataの値を1つずつチャネルに送信する
	loopData := func(ch chan<- int) {
		defer close(ch)
		for i := range data {
			ch <- data[i]
		}
	}

	ch := make(chan int)
	go loopData(ch)

	// チャネルから受信した値を出力
	for v := range ch {
		fmt.Println(v)
	}
	// 実行結果
	// 10
	// 20
	// 30
}

コメントに書いた通り、変数 data にはどこかれでもアクセスできるが、loopData 関数からのみアクセスするように書かれている。この規約(ルール)がアドホック拘束。

しかし、開発を続けているうちに、この規約が破られて問題が起こる可能性が高い。

 

レキシカル拘束

レキシカル拘束のサンプル。

package main

import "fmt"

func main() {
	chanOwner := func() <-chan int {
		// 関数内でチャネルを初期化して、受信操作専用のチャネルを返す
		// こうすることでチャネルへ書き込みができるスコープを制限できる
		resultCh := make(chan int)
		go func() {
			defer close(resultCh)
			for i := 0; i < 3; i++ {
				resultCh <- i
			}
		}()
		return resultCh
	}

	// 受信操作専用のチャネルを受け取り、値を出力していく
	printer := func(resultCh <-chan int) {
		for v := range resultCh {
			fmt.Println(v)
		}
		fmt.Println("Done!!")
	}

	resultCh := chanOwner()
	printer(resultCh)
	// 実行結果
	// 0
	// 1
	// 2
	// Done!!
}

chanOwner 関数のレキシカルスコープ内で resultChチャネルを初期化して、受信操作専用のチャネルとして返している。こうすることで、resultChチャネルへ送信できるのは chanOwner 関数のみになる。

つまり、resultCh チャネルへの送信権限を拘束して、他のゴルーチンが誤った値を送信するのを防いでいる。

※ レキシカル拘束は、別にチャネルに限った話ではない。変数全般に対して言えること。レキシカルスコープを使って適切な範囲にのみ公開するように心がけると安全な処理が書ける。(特に並行処理では重要)

レキシカル拘束の例をもう一つ。

package main

import (
	"fmt"
	"sync"
)

func main() {
	sumPrinter := func(wg *sync.WaitGroup, data []int) {
		defer wg.Done()

		var sum int
		for _, v := range data {
			sum += v
		}

		fmt.Printf("sum = %d\n", sum)
	}
	var wg sync.WaitGroup
	data := []int{1, 2, 3, 10, 20, 30}

	wg.Add(2)
	go sumPrinter(&wg, data[3:]) // => wum = 6
	go sumPrinter(&wg, data[:3]) // => sum = 60
	wg.Wait()

	fmt.Println("Done!!")
}

この例では、

  • sumPrinter 関数は data スライスの宣言前に定義されているので直接アクセスできず、引数として受け取っている。
  • 呼び出し側のゴルーチンでは、data スライスの部分集合を渡しているので、sumPrinter 関数はスライスの一部しかアクセスできない

data スライスへのアクセス範囲を拘束している。

 

参考図書

Tour of Goを終えた人にオススメです!!

-Go言語

© 2021 フリエン生活 Powered by AFFINGER5