ふにゃるんv2

もとは、http://d.hatena.ne.jp/Wacky/

pyCDK(CDK)をマルチスレッド対応にしよう

前回( id:Wacky:20050828 )、pyCDKを突っ込んで遊んでいたんだけど、マルチスレッドを使うと片方が応答しなくなる事に気付いた。


例えば、ウィンドウを2枚作って、これをAウィンドウとBウィンドウとする。
片方のスレッドで Aウィンドウに対して、 activate メソッドを呼び出していると思いねぇ。

selection = scroll.activate()

んで、もう片方のスレッドで、Bウィンドウに対して addstr 関数を呼び出そうとすると、どうもロックがかかる模様。
(何で、こんな事したいかって言うと、Aウィンドウでメニュー的な処理をしつつ、Bウィンドウでロギング表示させたいと思ったからなのよ)


どうにかしてやりたいと思って、色々と調べていたら、 curses ライブラリを普通に使っている時、マルチスレッドで呼び出しが出来るという事に気付いた。
つまり、pyCDK(CDK)を使っている時のみ、ロックがかかるようなんですわな。


んじゃぁ、という訳で、cursesライブラリとpyCDK(CDK)ライブラリの違いを調べてみるか、とソースを眺めていたら、Pythonソースコード内の、 /Modules/_cursesmodule.c に答えがあった。

static PyObject *
PyCursesWindow_GetCh(PyCursesWindowObject *self, PyObject *args)
{
  int x, y;
  int rtn;

  switch (PyTuple_Size(args)) {
  case 0:
    Py_BEGIN_ALLOW_THREADS      // ← ここ
    rtn = wgetch(self->win);
    Py_END_ALLOW_THREADS        // ← ここ
    break;
...

以下の項を読むと、グローバルインタプリタロックを獲得しないといけない風に書いてある。

  • Python/C API リファレンスマニュアル

8.1 スレッド状態 (thread state) とグローバルインタプリタロック (global interpreter lock)
http://www.python.jp/doc/release/api/threads.html

グローバルインタプリタロックを獲得したスレッドだけが Python オブジェクトを操作したり、 Python/C API 関数を呼び出したりできるというルールがあります。マルチスレッドの Python プログラムをサポートするため、インタプリタは定期的に -- デフォルトの設定ではバイトコード 100 命令ごとに (この値は sys.setcheckinterval() で変更できます) -- ロックを解放したり獲得したりします。このロックはブロックが起こりうる I/O 操作の付近でも解放・獲得され、I/O を要求するスレッドが I/O 操作の完了を待つ間、他のスレッドが動作できるようにしています。

つまり、なんですわな。要するに Windows 3.1時代のスレッド処理を行えって事かいな?
という事は、pyCDK(つ〜か、pyCDKはCDKのラッパでしかないので、CDKの方ね)に対して、_cursesmodule.c と同じような処理をかましてやればOKって事だよね?
(独学なので、ここいら辺、全然わからん。う〜ん困った)


_cursesmodule.c のコードを眺めていると、 getch系と refresh系の関数に対して、Py_BEGIN_ALLOW_THREADS と Py_END_ALLOW_THREADS を呼び出している模様なので、CDKのコードに対しても、同じ事を行う。

修正した結果のパッチは、↓ここ。
id:Wacky:20000904

はてなダイアリーで、非画像の添付ファイルを付ける方法をよく知らないので、てけとーな時期の日記に、"diff -cr"したパッチコードを まんま貼り付けてみた。(約30kb)
こういうタイプのファイルって、何とかする方法って無いのかしらんねぇ?


で、検証コードな。


scroll.pyを改造したもの:

import os
import cdk
import curses
import threading
import time

class test_thread(threading.Thread):
    def __init__(self, win):
        self.win = win
        threading.Thread.__init__(self)
    def run(self):
        cnt = 0
        self.isRunning = True
        while self.isRunning:
            win.addstr(0, 0, "cnt=%d   " % (cnt))
            win.refresh()
            cnt = cnt + 1
            time.sleep(0.5)

title = "Choose a file\n"
choices = os.listdir('.')

try:
    win = curses.initscr()
    
    th = test_thread(win)
    th.setDaemon(True)
    th.start()
    
    screen = cdk.Screen(win)
    scroll = cdk.Scroll(screen, cdk.CENTER, cdk.CENTER, 15, 40, title, choices)
    selection = scroll.activate()
    if(scroll.getExitType() == cdk.vESCAPE_HIT):
        mesg = ["<C>You hit escape. No word was selected.",]
        cdk.popup(screen, mesg)
    else:
        mesg = ("<C>You selected the following: %s" % choices[selection],)
        cdk.popup(screen, mesg)
    
    th.isRunning = False
    th.join(2)
    
    scroll.destroy()
    screen.destroy()
finally:
    cdk.exitCDK()

実行すると、中央のメニューとは別に 上でカウンターが回る回る。うひょ。

注意点

  • パッチファイルは、CDKを展開して、"./configure"を呼び出した後に当てる事を前提にしている。
  • pythonのヘッダは、/usr/include/python2.4 にある前提にしている。(Makefileの所)
  • cdk.hを弄って、 Py_BEGIN_ALLOW_THREADS の効果を付けたり、外したりできる仕掛けを付けた(CDK_PYTHON_EXT)けど、make で define定義を付け足す方法を よく知らないので、結局 cdk.h に #define 行を入れちゃった。
  • pyCDKでは、CDKをビルドして生成される libcdk.a を内部に組み込む形で、 cdk.dll を生成する。
  • という訳で、cdk.dllを作った後、CDK_PYTHON_EXT を外してリビルドすれば、 libcdk.a は Pythonに無関係のライブラリにできちゃう。
  • CDKのコードを修正しリビルドした後、pycdkの再ビルドに反映する場合、pycdk/build 下の cdk.dll ファイルを削除しないと再更新してくれない。