初心者レベルの Python スレッドの問題
-
20-08-2019 - |
質問
Python (pyGTK を使用) での GUI 開発の初心者として、私はスレッドについて学び始めたところです。自分のスキルをテストするために、開始/停止ボタンを備えたシンプルな小さな GTK インターフェイスを作成しました。目標は、クリックされると、GUI の応答性を維持しながら、テキスト ボックス内の数値を迅速に増加させるスレッドを開始することです。
GUI は正常に動作していますが、スレッド処理に問題があります。おそらく単純な問題ですが、私の心はその日の揚げ物についてです。以下に、最初に Python インタープリターからのトラックバックを貼り付け、次にコードを貼り付けます。に行くことができます http://drop.io/pxgr5id ダウンロードしてください。リビジョン管理に bzr を使用しているため、変更を加えて再ドロップしたい場合は、変更をコミットしてください。コードも貼り付けています http://dpaste.com/113388/ なぜなら、行番号を付けることができ、このマークダウンのせいで頭が痛くなるからです。
1月27日15時52分(EST)更新:わずかに更新されたコードはここにあります。 http://drop.io/threagui/asset/thread-gui-rev3-tar-gz
トレースバック
crashsystems@crashsystems-laptop:~/Desktop/thread-gui$ python threadgui.pybtnStartStop clicked
Traceback (most recent call last):
File "threadgui.py", line 39, in on_btnStartStop_clicked
self.thread.stop()
File "threadgui.py", line 20, in stop
self.join()
File "/usr/lib/python2.5/threading.py", line 583, in join
raise RuntimeError("cannot join thread before it is started")
RuntimeError: cannot join thread before it is started
btnStartStop clicked
threadStop = 1
btnStartStop clicked
threadStop = 0
btnStartStop clicked
Traceback (most recent call last):
File "threadgui.py", line 36, in on_btnStartStop_clicked
self.thread.start()
File "/usr/lib/python2.5/threading.py", line 434, in start
raise RuntimeError("thread already started")
RuntimeError: thread already started
btnExit clicked
exit() called
コード
#!/usr/bin/bash
import gtk, threading
class ThreadLooper (threading.Thread):
def __init__ (self, sleep_interval, function, args=[], kwargs={}):
threading.Thread.__init__(self)
self.sleep_interval = sleep_interval
self.function = function
self.args = args
self.kwargs = kwargs
self.finished = threading.Event()
def stop (self):
self.finished.set()
self.join()
def run (self):
while not self.finished.isSet():
self.finished.wait(self.sleep_interval)
self.function(*self.args, **self.kwargs)
class ThreadGUI:
# Define signals
def on_btnStartStop_clicked(self, widget, data=None):
print "btnStartStop clicked"
if(self.threadStop == 0):
self.threadStop = 1
self.thread.start()
else:
self.threadStop = 0
self.thread.stop()
print "threadStop = " + str(self.threadStop)
def on_btnMessageBox_clicked(self, widget, data=None):
print "btnMessageBox clicked"
self.lblMessage.set_text("This is a message!")
self.msgBox.show()
def on_btnExit_clicked(self, widget, data=None):
print "btnExit clicked"
self.exit()
def on_btnOk_clicked(self, widget, data=None):
print "btnOk clicked"
self.msgBox.hide()
def on_mainWindow_destroy(self, widget, data=None):
print "mainWindow destroyed!"
self.exit()
def exit(self):
print "exit() called"
self.threadStop = 1
gtk.main_quit()
def threadLoop(self):
# This will run in a thread
self.txtThreadView.set_text(str(self.threadCount))
print "hello world"
self.threadCount += 1
def __init__(self):
# Connect to the xml GUI file
builder = gtk.Builder()
builder.add_from_file("threadgui.xml")
# Connect to GUI widgets
self.mainWindow = builder.get_object("mainWindow")
self.txtThreadView = builder.get_object("txtThreadView")
self.btnStartStop = builder.get_object("btnStartStop")
self.msgBox = builder.get_object("msgBox")
self.btnMessageBox = builder.get_object("btnMessageBox")
self.btnExit = builder.get_object("btnExit")
self.lblMessage = builder.get_object("lblMessage")
self.btnOk = builder.get_object("btnOk")
# Connect the signals
builder.connect_signals(self)
# This global will be used for signaling the thread to stop.
self.threadStop = 1
# The thread
self.thread = ThreadLooper(0.1, self.threadLoop, (1,0,-1))
self.threadCounter = 0
if __name__ == "__main__":
# Start GUI instance
GUI = ThreadGUI()
GUI.mainWindow.show()
gtk.main()
解決
PyGTK を使用したスレッド処理を正しく実行しようとすると、少し注意が必要です。基本的に、メインスレッド以外のスレッド内から GUI を更新しないでください (GUI ライブラリの一般的な制限)。通常、これは PyGTK でキューに入れられたメッセージ (ワーカーと GUI 間の通信用) のメカニズムを使用して行われ、タイムアウト機能を使用して定期的に読み取られます。このトピックについてローカル LUG でプレゼンテーションを行ったら、このプレゼンテーションのサンプル コードを次のサイトから取得できます。 Google コード リポジトリ. 。見て MainWindow
クラスイン forms/frmmain.py
, 、特にメソッドの場合 _pulse()
そしてそこで何が行われるか on_entry_activate()
(スレッドがそこで開始され、さらにアイドル タイマーが作成されます)。
def on_entry_activate(self, entry):
text = entry.get_text().strip()
if text:
store = entry.get_completion().get_model()
if text not in [row[0] for row in store]:
store.append((text, ))
thread = threads.RecommendationsFetcher(text, self.queue)# <- 1
self.idle_timer = gobject.idle_add(self._pulse)# <- 2
tv_results = self.widgets.get_widget('tv_results')
model = tv_results.get_model()
model.clear()
thread.setDaemon(True)# <- 3
progress_update = self.widgets.get_widget('progress_update')
progress_update.show()
thread.start()# <- 4
このようにして、アプリケーションは (GTK 手段による) 「アイドル」時に GUI を更新し、フリーズを引き起こしません。
- 1:スレッドを作成する
- 2:アイドルタイマーを作成する
- 3:スレッドをデーモン化して、スレッドの完了を待たずにアプリを閉じることができるようにする
- 4:スレッドを開始します
他のヒント
一般に、できる限りスレッドを回避することをお勧めします。スレッド アプリケーションを正しく作成することは非常に困難であり、正しく作成できたかどうかを確認するのはさらに困難です。GUI アプリケーションを作成している場合、非同期フレームワーク内でアプリケーションを作成する必要があるため、その方法を視覚化するのが簡単です。
理解すべき重要なことは、GUI アプリケーションはほとんど何も行っていないということです。ほとんどの時間は、OS から何かが起こったことを通知されるのを待つことに費やされます。ブロックされないように長時間実行されるコードを記述する方法を知っていれば、このアイドル時間に多くのことを行うことができます。
タイムアウトを使用すると、元の問題を解決できます。遅延後に何らかの関数をコールバックするように GUI フレームワークに指示し、その遅延をリセットするか、別の遅延呼び出しを開始します。
もう 1 つのよくある質問は、GUI アプリケーションでネットワーク経由で通信する方法です。ネットワーク アプリは、大量の待機を行うという点で GUI アプリに似ています。ネットワーク IO フレームワークを使用する (例: ツイスト) を使用すると、アプリケーションの両方の部分を競合するのではなく連携して待機させることが容易になり、やはり追加のスレッドの必要性が軽減されます。
長時間実行される計算は同期的ではなく反復的に記述でき、GUI がアイドル状態のときに処理を実行できます。Python ではジェネレーターを使用すると、これを非常に簡単に行うことができます。
def long_calculation(param, callback):
result = None
while True:
result = calculate_next_part(param, result)
if calculation_is_done(result):
break
else:
yield
callback(result)
電話をかける long_calculation
ジェネレーターオブジェクトが得られるので、呼び出します .next()
ジェネレーター オブジェクトでは、次のいずれかに到達するまでジェネレーターが実行されます。 yield
または return
. 。GUI フレームワークに呼び出すように指示するだけです。 long_calculation(some_param, some_callback).next
時間があるときに、最終的には結果とともにコールバックが呼び出されます。
私は GTK についてあまり詳しくないので、どの gobject 関数を呼び出す必要があるのかを説明できません。ただし、この説明があれば、必要な関数をドキュメントで見つけることができるか、最悪の場合、関連する IRC チャネルで問い合わせることができるはずです。
残念ながら、一般的な場合に適切な答えはありません。何をしようとしているのかを正確に明確にすれば、なぜその状況でスレッドが必要ないのかを説明するのが簡単になるでしょう。
停止したスレッド オブジェクトを再開することはできません。しようとしないでください。実際に停止して結合した後でオブジェクトを再起動する場合は、代わりに、オブジェクトの新しいインスタンスを作成します。
スレッドやアイドル処理などの作業をクリーンアップするために、さまざまなツールを試してきました。
make_idle は、バックグラウンドでタスクを連携して実行できるようにする関数デコレーターです。これは、UI スレッドで 1 回実行すればアプリの応答性に影響を与えない程度に短いものと、特別な同期で完全なスレッドを実行するものとの間の適切な中間点です。装飾された関数内では、「yield」を使用して処理を GUI に渡し、応答性を維持できるようにします。次回 UI がアイドル状態になったときに、中断したところから関数が再開されます。したがって、これを開始するには、装飾された関数に idle_add を呼び出すだけです。
def make_idler(func):
"""
Decorator that makes a generator-function into a function that will
continue execution on next call
"""
a = []
@functools.wraps(func)
def decorated_func(*args, **kwds):
if not a:
a.append(func(*args, **kwds))
try:
a[0].next()
return True
except StopIteration:
del a[:]
return False
return decorated_func
もう少し処理を行う必要がある場合は、コンテキスト マネージャーを使用して必要に応じて UI スレッドをロックし、コードを少し安全にすることができます。
@contextlib.contextmanager
def gtk_critical_section():
gtk.gdk.threads_enter()
try:
yield
finally:
gtk.gdk.threads_leave()
それであなたはただできます
with gtk_critical_section():
... processing ...
まだ完成していませんが、純粋にアイドル状態で実行することと純粋にスレッド内で実行することを組み合わせることで、yield の後の次のセクションを実行するかどうかを伝えることができるデコレーター (まだテストされていないため投稿されていません) ができました。 UI のアイドル時間またはスレッド内。これにより、UI スレッドで何らかのセットアップを実行し、バックグラウンド処理を行うために新しいスレッドに切り替えてから、クリーンアップを行うために UI のアイドル時間に切り替えて、ロックの必要性を最小限に抑えることができます。
コードを詳しく調べていません。しかし、私はあなたの問題に対する解決策が 2 つあると考えています。
スレッドは一切使用しないでください。代わりに、次のようにタイムアウトを使用します。
import gobject
i = 0
def do_print():
global i
print i
i += 1
if i == 10:
main_loop.quit()
return False
return True
main_loop = gobject.MainLoop()
gobject.timeout_add(250, do_print)
main_loop.run()
スレッドを使用する場合は、次のように保護して、GUI コードが同時に 1 つのスレッドからのみ呼び出されるようにする必要があります。
import threading
import time
import gobject
import gtk
gtk.gdk.threads_init()
def run_thread():
for i in xrange(10):
time.sleep(0.25)
gtk.gdk.threads_enter()
# update the view here
gtk.gdk.threads_leave()
gtk.gdk.threads_enter()
main_loop.quit()
gtk.gdk.threads_leave()
t = threading.Thread(target=run_thread)
t.start()
main_loop = gobject.MainLoop()
main_loop.run()