前回( 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; ...
以下の項を読むと、グローバルインタプリタロックを獲得しないといけない風に書いてある。
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 ファイルを削除しないと再更新してくれない。