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

プログラムを起動し、employeemanagerの各スレッドが作成された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.