Ruby/Qte普及委員会 非同期処理

XREAAD
Create  Edit  Diff  FrontPage  Index  Search  Changes  History  RSS  Login

非同期処理とは

「ソフトウェアの追加/削除」を実行すると、ザウルスにインストールされているか、またはInstall_Filesフォルダにあるパッケージ名がズラズラッと一覧表示されますが、そのとき、パッケージ名が一覧に追加されていく様子を眺めることができますよね。あれはどうやっているんでしょうか。

これは、一方にパッケージ情報を探索する処理Aがあり、もう一方に取得されたパッケージ情報を一覧に追加して表示を更新する処理Bがあって、両者が並行して実行するようになっていると思われます。

それだけでなく、Aは処理の途中でデータをBに渡せるようになっているはずです。そうでなければ、Aを行ってその後にBを行うのとあまり変わりがないことになります。

こういう処理を実現するための方法を考えてみます。

実は、拙作の ipkginfo は、この非同期処理を行っていないために、起動してから表示するまでずいぶん長く待たされます。これを改良することを目標にします。

非同期処理なしでも書ける?

よく考えたら、パッケージ情報を探索する処理Aのなかで、情報を得るたびに一覧を更新してやればいいじゃないか? と思うかもしれません。

たとえば、簡単な例ですが、

Dir.glob('/mnt/card/Documents/Install_Files/*.ipk') { |f|
  puts f
}

というスクリプトを書いて実行すれば、ファイル名がズラズラッと出るんだから、puts f を適当なコードに置き換えるだけで済むんじゃないか? ということです。

じゃあ実際にやってみましょう。

(001_ipkginfo.rb)

#!/usr/bin/env ruby
require 'qte'
require 'qpe'

module IpkgInfo
  include Qte
  include Qpe

  INSTALL_FILES = '/mnt/card/Documents/Install_Files/*.ipk'

  class ListView <QListView
    def initialize(parent = nil, name = '')
      super(parent, name)
      addColumn('filename')
    end
  end

  class InfoList

    include Enumerable

    def each(&block)
      Dir.glob(INSTALL_FILES) {|f|
	yield(f)
	sleep(1)
      }
    end
  end

  class MainWindow <QMainWindow
    def initialize(parent = nil, name = '')
      super(parent, name)
      setCaption('IpkgInfo')
      @listView = ListView.new(self)
      @infoList = InfoList.new
      setCentralWidget(@listView)
    end

    def setupInfoList
      @infoList.each {|i|
	QListViewItem.new(@listView, i)
      }
    end
  end
end

if $0 == __FILE__ then
  include Qte
  include Qpe
  a = QPEApplication.new([$0] + ARGV)
  mw = IpkgInfo::MainWindow.new
  a.showMainWidget(mw)
  mw.setupInfoList
  a.exec
end

先の、Install_Files ディレクトリにある *.ipk ファイルをリストアップする処理は、IpkgInfo::InfoList? クラスの each メソッドに置きました。一瞬で終わってしまうと動作の違いがわからないので、1ファイル見つけるたびに1秒スリープするようにしました。

まずはテストです。以下の test_ipkginfo.rb を書いて、IpkgInfo::InfoList?#each が正しく動作しているかどうか確かめます。

require '001_ipkginfo'

infoList = IpkgInfo::InfoList.new

infoList.each {|i|
  puts i
}

たしかに、1秒ぐらいの間隔で順繰りにファイルが表示されていきます。

さて、IpkgInfo::ListView? にこの処理の結果を追加していくには、IpkgInfo::InfoList?#each をどこで呼び出せばよいのでしょう?

001_ipkginfo.rb ではとりあえず、IpkgInfo::MainWindow?#setupInfoList というメソッドでIpkgInfo::InfoList?#eachを呼び出し、先のテストコードの puts に相当する部分を、QListViewItemを生成してリストビューに追加するように変えました。

この setupInfoList はどこに置けばいいんでしょうか? 001_ipkginfo.rb では、スクリプトのトップレベルで、a.showMainWidget(mw) の後に mw.setupInfoList と書いています。これでうまくいくでしょうか?

ruby 001_ipkginfo.rb

ずいぶん待たされたあげくに、ズラズラッではなくて一瞬にサッと表示されます。

実は、メインウィジェットのイベントループは a.exec によって開始されるので、それまでは、ウィジェットへの入力はもちろん、表示もされないのです。

じゃあ exec の処理中に、IpkgInfo::ListView? が表示された後のタイミングで IpkgInfo?::MainWindow?#setupInfoList を呼び出せばいいんじゃないでしょうか。

Qtのドキュメントをたぐっていくと、QListViewの祖先クラスのQFrameにpaintEventというバーチャル関数があるのがわかります。これを上書きして、リストビューを初めて表示するときに firstDrawn というシグナルを送るようにしてみましょう (002_ipkginfo.rb)

(002_ipkginfo.rb の一部)

  class ListView <QListView
    def initialize(parent = nil, name = '')
      super(parent, name)
      addColumn('filename')
      @firstDrawn = RSignal.new
      @isDrawn = false
      catchEvent
    end

    attr_reader :firstDrawn

    def paintEvent(ev)
      super(ev)
      unless @isDrawn
	@firstDrawn.send
	@isDrawn = true
      end
    end
  end

そして、IpkgInfo::MainWindow? のオブジェクトが firstDrawn シグナルを受け取ると、setupInfoList を実行するようにします。トップレベルで呼び出していたほうはコメントアウトします。

  class MainWindow <QMainWindow
    def initialize(parent = nil, name = '')
      super(parent, name)
      setCaption('IpkgInfo')
      @listView = ListView.new(self)
      @infoList = InfoList.new
      setCentralWidget(@listView)
      connect(@listView.firstDrawn, self, "setupInfoList")
    end
if $0 == __FILE__ then
  include Qte
  include Qpe
  a = QPEApplication.new([$0] + ARGV)
  mw = IpkgInfo::MainWindow.new
  a.showMainWidget(mw)
  # mw.setupInfoList
  a.exec
end

実行してみます。

# ruby 002_ipkginfo.rb
002_ipkginfo.rb:23:in `paintEvent': super: no superclass method
 `paintEvent' (NameError)
	from 002_ipkginfo.rb:67:in `exec'
	from 002_ipkginfo.rb:67

エラーが出てしまいました……。paintEvent内でスーパークラスの同名メソッドを呼び出す super が使えないとなると、自分で実装しなけりゃならないんでしょうか…(このあたりよくわからないので、ご教示いただければ幸いです)。

……というわけで、なかなかいいところまでは行ったのですが、結局つまづいてしまいました。

Rubyのスレッドを使う方法

Rubyのスクリプトを書いた経験のある方なら、Rubyのスレッド機能を使うのがてっとりばやいでしょう。

001_ipkginfo.rb に戻って、トップレベルのコードを次のように修正します (003_ipkginfo.rb)。

if $0 == __FILE__ then
  require 'thread'
  include Qte
  include Qpe
  a = QPEApplication.new([$0] + ARGV)
  mw = IpkgInfo::MainWindow.new
  a.showMainWidget(mw)
  Thread.start {
    mw.setupInfoList
  }
  a.exec
end

threadライブラリをrequireし、非同期に実行させたい箇所を Thread.start { ... } で囲みました。

それでは実行してみましょう。

ruby 003_ipkginfo.rb

10秒ぐらい待っていると、何もないリストビューが表示され、その後にファイル名が順々に加わっていきます。

問題点

Rubyのスレッド機能を使うと、このように非同期処理を簡潔に書くことができます。ただし、スレッド間でメモリが共有されているために競合が起こり、それによって深刻なバグが潜む可能性があります。このあたりは、Ruby本の第7章が検討の手がかりになるでしょう。(つづく)

タイマ (QTimer) を使う方法

タイマを使う方法もあります。一定間隔で細かい処理を繰り返したり(インターバルタイマ)、指定した時間が経過したら処理を始めさせる(シングルショットタイマ)というやり方です。

まずは、シングルショットタイマを試してみましょう。001_ipkginfo.rb のトップレベルのコードを次のように書き換えてみます。

if $0 == __FILE__ then
  require 'thread'
  include Qte
  include Qpe
  a = QPEApplication.new([$0] + ARGV)
  mw = IpkgInfo::MainWindow.new
  a.showMainWidget(mw)
  timer = QTimer.new(a)
  connect(timer, QSIGNAL("timeout()"), mw, "setupInfoList")
  timer.start(5000, true)
  a.exec
end

QTimer#start の第一引数には、タイムアウト時間をミリ秒単位で指定します。第二引数はシングルショットタイマかどうかを指定します。また、Qte::connectメソッドを使って、タイムアウト時に実行するスロットを指定します。上記のコードでは、5秒後に mw.setupInfoList を1回だけ実行するように指定したことになります。

これを実行してみるとどうなるでしょうか。10秒後ぐらいにウィンドウが表示された後、しばらく待たされて、それからサッと一瞬でファイル名が一覧表示されるはずです。

実は、mw.setupInfoListは並行動作しているわけではなく、5秒後にmw.setupInfoListに処理が移っているだけなのです。QTimerと結びつけて実行する処理は、長い時間がかかるものではなく、すぐに終わるような細かいものでなければなりません。

処理を細かく分けるためにはどうしたらいいでしょうか。setupInfoList の each { ブロック } のところを見直す必要があります。ここは、どうあがいても時間のかかる処理を細かく分けることができません。

そこで、IpkgInfo::InfoList? クラスの設計を見直すことにします。each のほかにもう一つ、ファイル名を1つずつ取り出す getOne というメソッドを加えることにします。

(004_ipkginfo.rb の一部)

    def getOne
      unless @files
	@files = Dir.glob(INSTALL_FILES)
      end
      sleep(1)
      @files.shift
    end

これは、Dir.glob が戻ってくるのにそれほど時間がかからないということを前提にしています。ただ、時間がかかるのは最初の1回だけで、2回目以降は配列をシフトしているだけなので、とりあえず気にしないことにします。また、配列が空になれば nil が返ります。

そして、IpkgInfo::MainWindowのほうも、ファイル名を1個取り出してリストビューに加えるメソッドを追加します。それと同時に、情報リストの読み込みが終わったら setupDone というシグナルを送出することにします。

(004_ipkginfo.rb の一部)

  class MainWindow <QMainWindow
    def initialize(parent = nil, name = '')
      super(parent, name)
      setCaption('IpkgInfo')
      @listView = ListView.new(self)
      @infoList = InfoList.new
      setCentralWidget(@listView)
      @setupDone = RSignal.new
    end

    attr_reader :setupDone
    def setupInfoListOne
      i = @infoList.getOne
      if i then
	QListViewItem.new(@listView, i)
      else
	@setupDone.send
      end
    end

これで、処理を細かく分けることができました。最後に、トップレベルのコードを次のように変えます。

(004_ipkginfo.rb の一部)

if $0 == __FILE__ then
  include Qte
  include Qpe
  a = QPEApplication.new([$0] + ARGV)
  mw = IpkgInfo::MainWindow.new
  a.showMainWidget(mw)
  timer = QTimer.new(a)
  connect(timer, QSIGNAL("timeout()"), mw, "setupInfoListOne")
  connect(mw.setupDone, timer, QSLOT("stop()"))
  timer.start(0, false)
  a.exec
end

QTimerをインターバルタイマにするときは、startの第二引数をfalseにします。また、第一引数を0にすると、イベントキューに何も処理するものがないときにタイムアウトするようになります。

全部のファイル名が取り出せた後も mw.setupInfoListOne が呼び出されるのは無駄なので、mw.setupDone シグナルが送出されたらタイマを停止するようにします。

実行してみましょう。

ruby 004_ipkginfo.rb

今度は、ズラズラッとファイル名が表示されたはずです。

  • paintEvent 内で super が呼べない件はちょっと時間がかかるかもしれませんが調べてみようと思います。ただ、この手順だと、非同期に追加とは呼べない(追加している間画面表示はともかく操作できない)と思うのですがそれは意図的なんでしょうか。 -- hdk 2003-09-11 (木) 03:16:09
  • ありがとうございます。非同期処理その2 として、一覧表示を途中でキャンセルするボタンを追加するケースを書いてみます。 -- noir 2003-09-11 (木) 07:01:38
{{comment}}
Last modified:2012/02/03 05:12:35
Keyword(s):
References:[プログラミングTips] [非同期処理その2]