byebugのガイドをおおざっぱになぞってみる(5)
GUIDE.mdに沿って実際に動かしてみて、自分で自分に説明してみた記録。翻訳ではない。(というか英語的にどうかって言われると自信がない。)
Byebugのバージョンは9.0.6、Rubyのバージョンは2.4.0p0。
GUIDE.mdはIntroductionに続いてGetting in & outへと進むが、ひとまずここまで。
Introduction (続き)
Threading support
Byebugはスレッドを使ったプログラムのデバッグに対応している。
次のサンプルプログラムを考えよう:
class Company
def initialize(task)
@tasks, @results = Queue.new, Queue.new
@tasks.push(task)
end
def run
manager = Thread.new do
manager_routine
end
employee = Thread.new do
employee_routine
end
sleep 6
go_home(manager)
go_home(employee)
end
#
# 従業員が働く
#
def employee_routine
loop do
if @tasks.empty?
have_a_break(0.1)
else
work_hard(@tasks.pop)
end
end
end
#
# 管理職が働く
#
def manager_routine
loop do
if @results.empty?
have_a_break(1)
else
show_off(@results.pop)
end
end
end
private
def show_off(result)
puts result
end
def work_hard(task)
task ** task
end
def have_a_break(amount)
sleep amount
end
def go_home(person)
person.kill
end
end
Company.new(10).run
コードを読み易くするために簡略化している。ここでは業務案件を「待ち行列」の中の数値として表現する。成果物も数値だ。従業員の仕事は与えられた数値を使った計算で、管理職の仕事は計算結果を画面に表示することだ。
最初の業務案件を用意して新しい会社を立ち上げよう。しばらく会社を運営すれば業務の成果が表示される。そのはずなんだがそうならない。
それではデバッグだ。
[1, 10] in /private/tmp/company.rb
=> 1: class Company
2: def initialize(task)
3: @tasks, @results = Queue.new, Queue.new
4:
5: @tasks.push(task)
6: end
7:
8: def run
9: manager = Thread.new do
10: manager_routine
(byebug) l
[11, 20] in /private/tmp/company.rb
11: end
12:
13: employee = Thread.new do
14: employee_routine
15: end
16:
17: sleep 6
18:
19: go_home(manager)
20: go_home(employee)
(byebug) c 17
Stopped by breakpoint 1 at /private/tmp/company.rb:17
[12, 21] in /private/tmp/company.rb
12:
13: employee = Thread.new do
14: employee_routine
15: end
16:
=> 17: sleep 6
18:
19: go_home(manager)
20: go_home(employee)
21: end
プログラムを起動し、employee
とmanager
の各スレッドが作成された17行目までプログラムを進めた。
(メモ: continue
命令は引数に行番号を指定すると、その場所まで実行を進める。)
作成されたスレッドはthread list
命令(略してth l
)を使って確認できる。
(byebug) th l
+ 1 #<Thread:0x007feaa507ca50 run> /private/tmp/company.rb:17
2 #<Thread:0x007feaa520e260@/private/tmp/company.rb:9 sleep_forever> /private/tmp/company.rb:9
3 #<Thread:0x007feaa520d608@/private/tmp/company.rb:13 sleep_forever> /private/tmp/company.rb:13
これら両方のスレッドで何が起きているか確認しバグを見付け出すことがここでの目的だ。
まずはemployee
スレッドに注目する。そのためにthread switch 3
という命令(略してth sw 3
)を実行し、スレッド内部に侵入する。
(byebug) th switch 3
3 #<Thread:0x007feaa520d608@/private/tmp/company.rb:13 sleep_forever> /private/tmp/company.rb:13
[9, 18] in /private/tmp/company.rb
9: manager = Thread.new do
10: manager_routine
11: end
12:
13: employee = Thread.new do
=> 14: employee_routine
15: end
16:
17: sleep 6
18:
thread switch
命令は引数で指定された番号のスレッドに移動する。
employee
スレッドの番号が3だというのはthread list
で表示から分かる。
thread list
の表示にはスレッドがどのファイルのどの行で作成されたかが表示されている。また、(Ruby 2.2.1以降なら)そのスレッドのどこを実行しているかも表示される。
続いてthread stop
命令(略してth st
)を使って、メインスレッド(番号1)とmanager
スレッド(番号2)の動きを止める。これで他のスレッドの動作の影響なくせるし、うっかりメインスレッドが終了してプログラムそのものが終了してしまわないようにできる。
ストップしているスレッドには$
マークが表示される。Byebugが今いるスレッドには+
マークが表示される。
(メモ: Byebugがいないスレッドもストップしているが、Byebugが今いる場所で実行を進めると、それに合わせて他のスレッドにも動作するチャンスが与えられる。これを避けるためにthread stop
命令で他のスレッドの動きを止めている。)
(byebug) th stop 1; th stop 2
$ 1 #<Thread:0x007f854987ca50 sleep_forever> /private/tmp/company.rb:17
$ 2 #<Thread:0x007f854a011ab8@/private/tmp/company.rb:9 sleep_forever> /private/tmp/company.rb:10
(byebug) th l
$ 1 #<Thread:0x007f854987ca50 sleep_forever> /private/tmp/company.rb:17
$ 2 #<Thread:0x007f854a011ab8@/private/tmp/company.rb:9 sleep_forever> /private/tmp/company.rb:10
+ 3 #<Thread:0x007f854a0101b8@/private/tmp/company.rb:13 run> /private/tmp/company.rb:14
それでは従業員の働きを追跡しよう。
(byebug) s
[22, 31] in /private/tmp/company.rb
22:
23: #
24: # 従業員が働く
25: #
26: def employee_routine
=> 27: loop do
28: if @tasks.empty?
29: have_a_break(0.1)
30: else
31: work_hard(@tasks.pop)
(byebug) s
[23, 32] in /private/tmp/company.rb
23: #
24: # 従業員が働く
25: #
26: def employee_routine
27: loop do
=> 28: if @tasks.empty?
29: have_a_break(0.1)
30: else
31: work_hard(@tasks.pop)
32: end
(byebug) n
[26, 35] in /private/tmp/company.rb
26: def employee_routine
27: loop do
28: if @tasks.empty?
29: have_a_break(0.1)
30: else
=> 31: work_hard(@tasks.pop)
32: end
33: end
34: end
35:
案件が届いていた。
(byebug) s
[51, 60] in /private/tmp/company.rb
51: def show_off(result)
52: puts result
53: end
54:
55: def work_hard(task)
=> 56: task ** task
57: end
58:
59: def have_a_break(amount)
60: sleep amount
(byebug) s
[23, 32] in /private/tmp/company.rb
23: #
24: # 従業員が働く
25: #
26: def employee_routine
27: loop do
=> 28: if @tasks.empty?
29: have_a_break(0.1)
30: else
31: work_hard(@tasks.pop)
32: end
次の案件はあるかな。
(byebug) n
[24, 33] in /private/tmp/company.rb
24: # 従業員が働く
25: #
26: def employee_routine
27: loop do
28: if @tasks.empty?
=> 29: have_a_break(0.1)
30: else
31: work_hard(@tasks.pop)
32: end
33: end
(byebug) n
[23, 32] in /private/tmp/company.rb
23: #
24: # 従業員が働く
25: #
26: def employee_routine
27: loop do
=> 28: if @tasks.empty?
29: have_a_break(0.1)
30: else
31: work_hard(@tasks.pop)
32: end
next
命令やstep
命令を使ってループをまわしてみる。最初の案件はを終えると、その後は新たな案件が届くのを休み休み待っているのを確認できる。
(メモ: next
命令(略してn
)の説明がなかなか出てこない。step
命令もnext
命令もプログラムの実行を進める命令だ。step
命令は実行する行でメソッド呼び出しがあればその内部に踏み込むのに対し、next
命令は踏み込まず表示の上での次の行に進む。)
問題なさそうだ。次は管理職だ:
(byebug) th resume 2
2 #<Thread:0x007f854a011ab8@/private/tmp/company.rb:9 run> /private/tmp/company.rb:10
(byebug) th switch 2
2 #<Thread:0x007f854a011ab8@/private/tmp/company.rb:9 sleep_forever> /private/tmp/company.rb:10
[5, 14] in /private/tmp/company.rb
5: @tasks.push(task)
6: end
7:
8: def run
9: manager = Thread.new do
=> 10: manager_routine
11: end
12:
13: employee = Thread.new do
14: employee_routine
thread resume
命令(略してth r
)は止めておいたスレッドを再び動かす。
thread resume
してからthread switch
するという順番はとても重要だ。そうしないとすべてが停止してしまう。
さあ管理職側から見て問題がないか調査しよう:
(byebug) s
[35, 44] in /private/tmp/company.rb
35:
36: #
37: # 管理職が働く
38: #
39: def manager_routine
=> 40: loop do
41: if @results.empty?
42: have_a_break(1)
43: else
44: show_off(@results.pop)
(byebug) s
[36, 45] in /private/tmp/company.rb
36: #
37: # 管理職が働く
38: #
39: def manager_routine
40: loop do
=> 41: if @results.empty?
42: have_a_break(1)
43: else
44: show_off(@results.pop)
45: end
成果物はあるかな……
(byebug) n
[37, 46] in /private/tmp/company.rb
37: # 管理職が働く
38: #
39: def manager_routine
40: loop do
41: if @results.empty?
=> 42: have_a_break(1)
43: else
44: show_off(@results.pop)
45: end
46: end
……まだない。
(byebug) n
[36, 45] in /private/tmp/company.rb
36: #
37: # 管理職が働く
38: #
39: def manager_routine
40: loop do
=> 41: if @results.empty?
42: have_a_break(1)
43: else
44: show_off(@results.pop)
45: end
(byebug) n
[37, 46] in /private/tmp/company.rb
37: # 管理職が働く
38: #
39: def manager_routine
40: loop do
41: if @results.empty?
=> 42: have_a_break(1)
43: else
44: show_off(@results.pop)
45: end
46: end
……まだない。
(byebug) n
[36, 45] in /private/tmp/company.rb
36: #
37: # 管理職が働く
38: #
39: def manager_routine
40: loop do
=> 41: if @results.empty?
42: have_a_break(1)
43: else
44: show_off(@results.pop)
45: end
おや? 変数@results
がずっと空ではないか。
従給員は成果物を管理職に提出するのは忘れてしまったようだ。
この問題を修正するにはemployee_routine
メソッドの中の
work_hard(@tasks.pop)
この部分を
@results << work_hard(@tasks.pop)
このように変更すればよさそうだ。
続く……
- More complex examples with objects, pretty printing and irb.
- Line tracing and non-interactive tracing.
- Post-mortem debugging.