ナレッジワークの Enablement Internship for Gophers に参加してきました!
インターンの内容等を振り返っていきたいと思います。
インターンシップ概要
〜Go上級者向け:バグのない並行処理の設計・実装編〜
期間: 3days
報酬: 2000/h
Goにおける並行処理について学習し、並行処理プログラミングで用いるライブラリやツールをOSSとして開発しました。
チームに一人ずつメンターがついており、なおかつ Gophers Japan 代表のtenntennさんにいつでも自由に質問することができるという高待遇っぷりでした。
上級者向けとあって内容も参加者のレベルも高く、3日間でかなり成長できたと思います!
day1
1日目は少しtenntennさんの講義を受けた後、3人チームでモブプログラミングをしました。
チーム課題としては、お題が二つに分かれており、私たちのチームは、net.Dial
関数の上位互換として、引数にコンテキストを追加しコンテキストのキャンセルを検知して、良しなにしてくれるDialWithContext
関数を作成せよ!、という課題を選びました。
(もう一つはsync.OnceFunc
の再実装課題でした。)
自分の知識としては、
Goのネットワークコネクションって何してどうするんだっけ???
net.Dial
関数なんて聞いたこともないぞ!
って状態からスタートしました...
まずは、新しくGo1.21で導入されるcontext.AfterFunc
関数を使用して、キャンセルされたらコネクションを閉じるという方針で実装を行いました。
net.Conn
型のSetDeadline
メソッドを使用して読み書きだけキャンセルするという実装方針もありましたが、そもそもキャンセルが呼ばれているならコネクションを再利用するとは考えにくいという判断で、SetDeadline
を使用せずにそのままコネクションをクローズする実装方針にしました。
その後、発展課題として、context.AfterFunc
関数を使用せず、Go1.20以下にも対応するにはどうすればいいかというお題が出ました。
最初はDialWithContext
の中で新しくgoroutineを立ち上げて、select case 文で <-ctx.Done()
のみを使用して実装しました。挙動としては正しく動作していたのですが、キャンセルされずに先にコネクションがクローズされた場合、goroutineが終了しないため、リークが発生してしまいました。
リーク回避として、DialWithContext
の戻り値にgoroutineをクローズできるcancelFunc
関数を用意して、もしクローズしたい場合はcancelFunc
関数を使用してもらうという実装にしました。
context.AfterFunc
関数を使用しないと使いづらくなってしまいました...
無事時間内にチーム課題を終えることができました。
自分一人では、間違いなく実装に詰まっていたのでモブプロ最高だ!と終止感謝してました。
モブプロ経験がなく最初は不安だったのですが、メンバーに恵まれたおかげもあり、初日としては良いスタートが切れました。
day2 & day3
残り二日間は個人開発を行いました。
テーマである並行処理関連のライブラリやツールを開発せよ! というものでした。
私は初学者が並行処理を学びやすいように、一行で使えるgoroutineデバッグツールを作成しました。
このツールを作成しようと思った背景には、runtime/trace
のような高機能なgoroutine可視化ツールがある一方で、初期設定のタスクやリージョン設定が面倒だと感じていました。
そのため、より手軽に使用できて、最終出力ではなくプログラム実行中のgoroutine状況を示すprint debugのようなツールがあれば便利だと思い、開発に至りました。
goroutineのスタックトレースを取得できる runtime.Stack
を使用しました。
この関数は Go 1.21 から各goroutineを作成した親のIDが含まれるようになったので、作成場所も示すことができるようになりました。
しっかしスタックトレースは慣れないと読みづらかったです!↓
goroutine 19 [running]:
main.goroutineSecond()
/tmp/sandbox4034358706/prog.go:35 +0x3d
main.goroutineFirst.func1()
/tmp/sandbox4034358706/prog.go:27 +0x49
created by main.goroutineFirst in goroutine 18
/tmp/sandbox4034358706/prog.go:25 +0x65
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000096020?)
/usr/local/go-faketime/src/runtime/sema.go:62 +0x25
sync.(*WaitGroup).Wait(0x516288?)
/usr/local/go-faketime/src/sync/waitgroup.go:116 +0x48
main.main()
/tmp/sandbox4034358706/prog.go:18 +0x6f
goroutine 18 [semacquire]:
sync.runtime_Semacquire(0xc000096030?)
/usr/local/go-faketime/src/runtime/sema.go:62 +0x25
sync.(*WaitGroup).Wait(0x0?)
/usr/local/go-faketime/src/sync/waitgroup.go:116 +0x48
main.goroutineFirst()
/tmp/sandbox4034358706/prog.go:30 +0x6f
main.main.func1()
/tmp/sandbox4034358706/prog.go:15 +0x49
created by main.main in goroutine 1
/tmp/sandbox4034358706/prog.go:13 +0x65
なので、取得したスタックトレースをパースして、あらかじめ作成しておいた構造体に格納し、その情報をわかりやすい形で出力するようにしました。
困ったことに、
goroutineの状態で[semacquire]や[chan send]など様々なラベルが存在しますが、散々調べても一覧が出てきませんでした。
runtime2.goのソースコードまで見に行き、待機理由の一覧を宣言しているものは見つけることができました。
もしstat情報の一覧を記載しているものがあれば、教えていただきたいです。
何はともあれ、成果発表までに完成することができました!
発表スライドは配布されたテンプレートを元に作成できるのでギリギリまで開発に集中できました。
成果発表も無事終わり、あっという間のインターンシップでした。
個人開発初日は完全に迷走していましたがメンターさんと話したおかげで何とか方向性が決まり、制作物となるものが出来上がりました。メンターさんに感謝です!!
学んだこと
普段あまり使用することの無い並行処理について深く学ぶことができました!
また2023年6月現在最新のGo1.21RC2環境を使用したので、日本語の記事が無く、強制的にドキュメントやソースコードを読む力が鍛えられました。
感想
モブプログラミングに対して、自分の思い通りに開発できず大変なだけでは?というマイナスイメージがありましたが全然そんなこと無かったです。
普段経験できないモブプロが体験できて良かった〜!
今まで大学然り、自分で何か作る場合も他の言語を使用していましたが、Goの方が書いていて面白かったので、今後はGoもどんどん使用していこうと思いました。
技術力が鍛えられ、すごい人がメンターに付いていただき、報酬まであるという素晴らしいインターンでした!