読者です 読者をやめる 読者になる 読者になる

もぐてっく

人は1つ歳を取る度、1ビットづつ大きくなれると信じてた。

mikutterにおけるスレッドと並行処理機構のまとめ

mikutterの薄い本Vol.8に掲載したmikutter-datasource-aclogを入れてから、なんかたまにGUIが重たくなる気がして来ました。

原因を調べたところ、mikutterイベントon_period内のhttp通信処理がたまに重くなるのせいで、メインスレッドのGUIの動作を阻害していたためでした。

てっきり別スレッドで動いてると思ってたよ。あらあらうふふ

いい機会なので、mikutterに於けるスレッドの取り扱いと、mikutterフレームワークの並列機構との関係を整理してみようと思いました。

mikutterに於けるスレッド

mikutterのスレッドは「メインスレッド」と「サブスレッド」に大別されます。
特徴はそれぞれこんな感じです。

メインスレッド

Rubyがデフォルトで持っているスレッドです。
mikutterにおけるメインスレッドは、初期化処理の後にGUIを司るGTKのメインループに支配されます。

mikutterのコアやプラグインは、後述のDelayerやGTKのシグナル処理を使ってメインスレッドで処理を行います。
メインスレッドの注意点は、インターネット通信などの「重い処理」を行うと、その分GUI処理が滞って「もっさり」や「プチフリ」を引き起こすことです。

サブスレッド

ここでは便宜上メインスレッドでないスレッドを「サブスレッド」と呼称します。
mikutterフレームワークではスレッドの使用に制限はなく、プラグインでサブスレッドを適宜生成して使用可能です。
サブスレッドはGUI処理を阻害せず「重い処理」を行える一方、サブスレッドからGTKGUI部品を操作することは出来ません。

mikutterフレームワークの並行処理機構

さて、各スレッドの得手不得手が整理できた所で、次はmikutterで使える並行処理について確認していきましょう。

Delayer

与えたブロック(処理)をメインスレッドで実行します。
DelayerはGTKのメインループと協調を取っており、GUI処理がひと段落するタイミングまで実行が保留される所がちょっとだけ注意です。
(/core/plugin/gtk/delayer.rbのGtk::idle_add_priority()で暇になった時にDelayerを実行する処理を設定しています。)

この性質を利用して、擬似的にサブスレッドからGUIの操作が行えるようになります。

Thread.new {
  # サブスレッドでなんか重い処理

  Delayer.new {
    # メインスレッドでGUI操作
  }
}

Reserver

Reserverは引数に指定した時間待った後にブロックを実行する機構です。
Reserverは専用のスレッドで時間待ちをしているため、Reserver処理もサブスレッドでの実行されます。
つまり、GUIをいじる場合はDelayerを使用する必要が有ります。

# 30秒後に処理を実行する
Reserver.new(30) {
  # ここはサブスレッドで実行される
}

イベント

mikutterイベントは内部でDelayerが噛むので、必ずメインスレッドで実行されます。

データを定期的に取得するのに便利なon_period(1分周期で発生)も例外ではなく、そこに冒頭のhttp処理なんかを書くと「もっさり」の原因となります。
(冒頭のやらかしですね。)

サブスレッドを作ってさっさと明け渡しましょう。

# 1分周期イベント
on_period { |service|
  # ここはメインスレッドなので、速やかに明け渡しましょう。
}

フィルタ

イベントと似た構文を持つフィルタは呼び出し元に値を返す必要があるため、処理が実行されるスレッドは呼び出し元と同じ(メイン、サブ特定できない)になります。

まぁ、フィルタ処理でスレッドやDelayerによる並行処理は原理上無意味なので、困ることはないと思います。

しかしながら、フィルタ内で重い処理を行うと後続の何らかの処理が遅れるはずなので、重い処理を伴うデータはReserverやon_periodなどで予め非同期に作っておくのが良いと思います。

on_period { |service|
  Thread {
    # 重い処理は予め実行しておく
    @data = heavy_proc()
  }
}

filter_hoge { |args|
  # フィルタでは@dataを使って答えを返すだけ(排他とかちゃんとしてね)
 [args]
}

GTKシグナル処理

signal_connect()やssc()で登録するGTKのシグナル処理はメインスレッドで動作します。
通常、プラグインからGTKシグナルを直接触ることがないため気にすることは無いですが、頭の配線から色々リークしているプラグイン作者は注意しましょう。

おまけ:Twitter APIは重い処理だよね?

Twitter APIも比較的低速な回線での通信が絡むので、重い処理と言えます。
しかし、mikutter独自のTwitter APIライブラリ「mikutwitter」では原則API呼び出しごとにサブスレッドを生成する仕様なので、「もっさり」が発生しない様になっています。

# APIをコールすると新たにサブスレッドが作られる
Service.primary.search(params).next{ |res|
  # Deferredによるスレッド実行後処理(next())もサブスレッドで動きます
}