不公平な業務フローから社員を救いながら分かった気になるGoの並行処理

6 min read読了の目安(約6100字

私は業務ではPython/javascript/Rubyを使うことが多いのですが、プライベートではGoが楽しくて学習しています!!
(ヨワヨワですがGoでAtCoderに参戦し始めたりしています)

言語によって特徴的な仕様や機能ってありますよね?
私見ですが

  • Rubyだったらメタプログラミング(メソッド探索順等を理解して使いこなせる)
  • javascriptだったら、プロトタイプチェーンを理解している

みたいなやつです。

Go言語の場合 gorutine を使いこなせていたらなんか格好良い気がします。

ということで書籍や記事で gorutine について学習してみたので、今回理解を深めるため記事を作成させていただきました。

といっても、 gorutine とは何か? どのようなパターンで実装すればよいか
というような記事は沢山良質なコンテンツがありますので省略させていただいています。

ストーリーを交えながらコードを書いてみたので、あまり小難しく考えずにふわっと読んでいただけますと幸いです。

※筆者は特定の業種や会社に恨みはありません。たまたま、思い浮かんだストーリーであるため、深く考えないでいただけると幸いです。(悪いのは業種ではなく、悪いことをする会社です。)

ストーリー

AB会社はシステム屋さんです。
Web系のシステム開発を得意としており、自社で受託開発を請け負っており、同時にSES事業でも収益を得ています。

しかし、AB会社の悪い営業が事実とは異なる条件で適当に契約を締結しては、後で客先からクレームが来ることが常態化していました。


悪い営業「エンジニアAはJavaの実務経験が3年あります」(3ヶ月も研修受けときゃ、一緒一緒)

悪い営業2「エンジニアBはWindowsServerを用いたシステム開発を基本設計から、実装までこなした経験があります!!」(Windos10でVBAしてたって言うから同じやろ)

悪い営業3「エンジニアCはプロマネの経験もあり、スクラム開発にも対応できるスペシャリストです!!」(ラグビー経験者だからギリセーフ)


悪い営業(座右の銘は弱肉強食)


そんな状態に激怒したAB会社社長は営業が顧客に渡す全ての資料・メールの二重チェックを行うことを命令しました。

社長「二重チェックしろ。システム導入?甘えんな。目で見ろ。神は細部に宿る。」


社長(好きな国は昔のインド。カースト制度を復興したい)


そこで不慣れではあるものの、
営業が資料を作成して、チェック係が精査・押印したものだけを顧客に渡せるように業務フローをやり直すことにしました。

コードで表してみる

そんなAB社の状態をGo言語で記載していきます。

まず営業・チェッカーどちらもAB社の社員です。

社員はそれぞれ違った仕事をしますが、家に帰るという人間として当然の権利を持っています。

// Document 書類
// 営業が内容を書いた後であれば isWritedがtrueに
// チェッカーが内容を精査した後であれば isCheckedがtrueになる
type Document struct {
	isWrited  bool
	isChecked bool
}

// Employee 社員です。社員はみな家に帰れます。早く帰らせてあげたい。
type Employee struct {
}

// GoHome 家に帰ります。ビール飲みたい。
func (employee Employee) GoHome() {
	fmt.Println("華金じゃあ!!!")
}

次に営業です。
営業は自分が担当する資料に見積もりを記載したり、約款を記載したりする必要があります。

// Sales 営業です。資料に必要な情報を記載するまでが仕事です。
// Document束を持ちます
type Sales struct {
	Employee //社員構造体の埋め込み。
	Documents []Document //担当する書類群
}

// WriteDocs 営業が営業資料に必要事項を記載します。
func (sales *Sales) WriteDocs() {
	for _, doc := range sales.Documents {
		doc.isWrited = true
	}
	// 終わったら帰宅する
	sales.Employee.GoHome()
}

次に今回の一番の被害者、チェック係を表す構造体です。
ちなみにプログラムには全く関係ありませんが、チェック係は総務係が兼任しています。
社長はチェックという金を産まない作業に人員を投入する気など一切ありません。

// Checker チェッカーです。営業が作った資料をチェックするのが仕事です。
// Documentの束を持ちます
type Checker struct {
	Employee
	Documents []Document
}

// CheckDocs チェッカーが営業が作った資料をチェックします。
func (checker *Checker) CheckDocs() {
	if len(checker.Documents) > 100 {
		fmt.Println("まとめて持ってくんなよ・・・")
	}
	for _, doc := range checker.Documents {
		if doc.isWrited {
			doc.isChecked = true
		}
	}
	// 終わったら帰宅する
	checker.Employee.GoHome()
}

そして営業係が資料を記載して、チェック係がチェックをするという状況をプログラムで表すとこんな感じでしょうか

func main() {
	// 何も書いていない書類を打ち出します
	documents := []Document{}
	for range make([]int, 1000) {
		newDoc := Document{}
		documents = append(documents, newDoc)
	}
	// 営業が資料に記載します
	sales := &Sales{Employee: Employee{}, Documents: documents}
	sales.WriteDocs()
	// 営業の資料を渡してチェッカーがチェックを始めます
	checker := &Checker{Employee: Employee{}, Documents: sales.Documents}
	checker.CheckDocs()
}

こちらを実行すると以下のように出力され、無事に営業もチェック係も帰宅できたようです。

華金じゃあ!!! //営業の叫び
まとめて持ってくんなよ・・・ //チェッカーのボヤキ
華金じゃあ!!! //チェッカーの叫び

ここで現実世界に戻って考えてみる

このプログラムの出力で気になったことがありましたね。

まとめて持ってくんなよ・・・

書類を100枚以上渡すとチェッカーがぼやきます。

現実世界でも同じような経験をしたことがある方はわかると思うのですが、
夕方頃にまとめて資料を持ってこられると本当に腹が立ちます。

集中して書類を作りたい営業の気持ちも分かりますが、このままではチェッカーの人は営業が全ての資料を作り終えるまで待機し続けて、まとめて処理をしなければならないです

チェッカーの人の要望をまとめると以下のような感じでしょう

「書類はまとめて渡さずに、逐一できた書類から持ってきてくれ」

待機し続けるのではなく、営業の書類作成作業と並行して作業をさせてほしいということですね

プログラムを改修してみる

「並行して」という言葉でタイトルを回収したところで、

  • 営業の資料作成
  • チェッカーのチェック

を並行作業にしてみます。

まずは、営業とチェッカーの間での書類の受け渡しをできるようにします。

type Sales struct {
	Employee
	Documents []Document
	DocStream chan Document //追加
}

type Checker struct {
	Employee
	DocStream chan Document //追加
}
// セールスには随時書類が送られてくるため書類の束を持たせないようにしています

ここの chan というのは channelという型です。
gorutine間でのデータを受け渡すためのパイプのようなものです。

ここでは

  • 営業とチェッカーの間に敷設された書類を運ぶためのベルトコンベア
  • 営業からチェッカーに書類を持っていってくれる新人の田中くん

とでも考えてもらったらよいかと思います(適当)


この会社で最も献身的な田中くん

チャンネルについては、このあたりの記事が非常にわかりやすかったです。

https://qiita.com/awakia/items/f8afa070c96d1c9a04c9
https://qiita.com/taigamikami/items/fc798cdd6a4eaf9a7d5e
https://gobyexample.com/channels

そして営業の書類作成とチェッカーの書類チェックのメソッドを以下のように修正します。


func (sales *Sales) WriteDocs(wg *sync.WaitGroup) {
	// 処理終了したら、チャンネルを閉じてもう書類がこないことを伝えます
	defer close(sales.DocStream)
	// 処理終了したら、このスレッドの処理は終わったから待たなくていいよと伝えます
	defer wg.Done()
	for _, doc := range sales.Documents {
		doc.isWrited = true
		sales.DocStream <- doc // 書類を随時送ってあげます
	}
	sales.Employee.GoHome()
}

func (checker *Checker) CheckDocs(wg *sync.WaitGroup) {
	defer wg.Done()
	// rangeで次々と送られてくる書類を受け取ってチェックしていきます
	for doc := range checker.DocStream {
		if doc.isWrited {
			doc.isChecked = true
		}
	}
	checker.Employee.GoHome()
}

チャンネルを通して、営業からチェッカーに書類が渡っていく様子が何となく分かるかと思います。

最後に呼び出す main関数を以下のように修正します。

func main() {
	// 営業とチェッカーの2つの並行処理が終わるのをmain関数で待ち受けるためのもの
	var wg sync.WaitGroup

	documents := []Document{}
	for range make([]int, 1000) {
		newDoc := Document{}
		documents = append(documents, newDoc)
	}
	//  資料受け渡しのためのチャンネルを作成します
	docStream := make(chan Document)
	// 営業が資料に記載します
	sales := &Sales{Employee: Employee{},
		Documents: documents,
		DocStream: docStream,
	}
	// 別スレッドで並行処理するから待つスレッド数を増やします
	wg.Add(1)
	go sales.WriteDocs(&wg)
	// 営業の資料を渡してチェッカーがチェックを始めます
	checker := &Checker{Employee: Employee{}, DocStream: docStream}
	wg.Add(1)
	go checker.CheckDocs(&wg)
	// 待つスレッド数がなくなるまで待ちます。
	wg.Wait()
}

上記コードでwgとか出てきていますが、

wg.Add(1) としているのは別スレッドでgorutineが起動するので、ここの処理が終わるまでmain関数が終わるのを待ってと伝えています。

defer wg.Done() で各営業の処理とチェッカーの処理が終わったら、このスレッドは待たなくていいよと伝えています。(待つスレッドが1つ減ります)

以上により以下のように出力されます。

華金じゃあ!!! //営業の叫び
華金じゃあ!!! //チェッカーの叫び

まとめ

このようにチャンネルと sync パッケージを利用して並行処理を作成してみました。

といっても私が学習したことを書いているだけなので、理解が間違えていたり、もっとスマートなやり方がある場合は教えて下さいますととても嬉しいです。

こんな駄文を読んでいただき、誠にありがとうございました。