Python/PySide: 如何销毁已结束的线程对象?

5
我希望能够实现一个按钮来停止一个进程中的线程,目前已经有了一些成果但并不如预期:我无法删除该线程对象。(编辑:似乎已经删除了对线程对象的引用,但是通过信号仍然可以访问它,而且信号没有自动断开连接。)
我有一个模块,其中包含一个名为thread_worker的类和一个用于复杂处理的函数,该函数正在作为进程运行:
from PySide.QtCore import *
from PySide.QtGui import *
import multiprocessing as mp
import time

# this function runs as a process
def complex_processing(queue):
    # do something
    ...

class thread_worker(QThread):
    message_signal = Signal(str)
    stop_thread_signal = Signal()

    def __init__(self, prozessID, sleepTime, parent=None):
        super(ThreadProzessWorker, self).__init__(parent)
        self.queue = mp.Queue()
        self.process = mp.Process(target=complex_processing, args=(self.queue,))
        self.timeStamp = int(time.time())

    def run(self):
        self.process.start()
        self.process.join()

    @Slot()
    def stop_process_and_thread(self):
        if self.isRunning():
            self.message_signal.emit("Thread %d is running!" % self.timeStamp)
            if self.process.is_alive():
                self.process.terminate()
                self.process.join()      
            self.stop_thread_signal.emit()   
            #self.terminate() # does it works at this place?       
        else:
            self.message_signal.emit("Thread %d is not running!" % self.timeStamp)

我的应用程序有两个按钮,一个是创建/运行线程对象,另一个是终止线程对象。

...
...
# Buttons
self.button_start_thread = QPushButton("Start Thread")
self.button_start_thread.clicked.connect(self.start_thread)
self.button_stop_thread = QPushButton("Stop Thread")
...
...
@Slot()
def start_thread(self):
    self.new_thread = thread_worker(self)
    self.button_stop_thread.clicked.connect(self.new_thread.stop_process_and_thread)
    self.new_thread.stop_thread_signal.connect(self.stop_thread)
    self.new_thread.message_signal.connect(self.print_message)
....
....
@Slot()
def stop_thread(self):
    self.new_thread.terminate()
    #self.button_stop_thread.disconnect(self.new_thread)
    del(self.new_thread)

@Slot(str)
def print_message(self, message):
    print(message)
...
...

如果我启动并停止第一个线程-它可以正常工作并终止,但是如果我再次点击“停止”按钮,输出结果如下:
Thread 1422117088 is not running!

我不太明白:对象 self.new_thread 是被 del(self.new_thread) 删除了吗?如果它已经被删除了,我该如何访问这个对象呢?如果我再次启动和停止一个新线程,输出会是什么:

Thread 1422117088 is not running!  # not expected, the thread object is deleted!
Thread 1422117211 is running!      # expected

现在我再次执行它(启动和停止),输出如下:
Thread 1422117088 is not running!   # not expected, the thread object is deleted!
Thread 1422117211 is not running!   # not expected, the thread object is deleted!
Thread 1422117471 is running!       # expected

第一个问题: 我不明白为什么旧的线程没有被删除?为什么我还可以访问它们?我认为这不好:如果后台有太多的线程(未删除的对象),我的应用程序会在某些时候崩溃。

第二个问题: 我不明白为什么如果我删除self.new_thread对象,信号不会被断开连接?我不想手动断开信号的连接:如果我有很多信号,我可能会忘记断开一些信号的连接。

第三个问题: 我选择使用这种方式来停止一个进程的线程。是否有更好的方法来做到这一点?

更新:

线程对象似乎已被销毁:

del(self.new_thread)
print(self.new_thread)
Output: AttributeError: 'MainWindow' object has no attribute 'new_thread'

但我的信号没有被断开!? 这里 描述了: "当涉及的任一对象被销毁时,信号槽连接将被删除。" 但在我的代码中它不起作用。


最后一次机会...移除print(self.new_thread),或将其包含在try-except子句中。 - finmor
我现在的代码中没有 print(self.new_thread),那只是为了检查对象是否被删除。我们看到,引用 self.new_thread 被删除了,但对象并没有被删除。 - Igor
3个回答

9

你是正确的,你碰到的问题是你的对象没有被删除。你只删除了引用self.new_thread。问题出在这一行:

self.new_thread = thread_worker(self)

这是因为线程的父级 self 仍然存活着。只要父级保持存活,对象 self.new_thread 就不会被销毁。可以尝试像这样做:
self.threadParent = QObject()
self.new_thread = thread_worker(self.threadParent)

现在您还需要删除父级self.threadParent

self.new_thread.terminate()
del(self.new_thread)
del(self.threadParent)

你现在应该已经断开了信号。

不需要以下这行代码,因为在发射 stop_thread_signal 信号后,对象 self.new_thread 已经被删除:

#self.terminate() # does it works at this place?

0
可能是因为您的进程没有包含任何内容并在启动后立即结束。因此,代码中出现了这种情况。
def stop_process_and_thread(self):
    if self.isRunning():
        ...
        if self.process.is_alive():
            self.process.terminate()
            self.process.join()      
            self.stop_thread_signal.emit()   
            #self.terminate() # does it works at this place?  

在 if self.process.is_alive() 后面的代码永远不会被执行。因此,stop_thread_signal 永远不会被发出,因此线程既不会被终止也不会被删除。

将您的测试 complex_processing 函数更改为

def complex_processing(queue):
# do something
  while(True):
    print "in process"
    time.sleep(0.2)

现在为了实现你想要的效果,你应该将线程存储在一个列表中,所以start_thread应该变成:
def start_thread(self):
    n = len(self.new_thread)
    self.new_thread.append(thread_worker(self))
    self.new_thread[-1].setTerminationEnabled(True)
    self.new_thread[-1].start()
    if n>0:
        self.button_stop_thread.clicked.disconnect()
    self.button_stop_thread.clicked.connect(self.new_thread[-1].stop_process_and_thread)
    print self.new_thread
    self.new_thread[-1].stop_thread_signal.connect(self.stop_thread)
    self.new_thread[-1].message_signal.connect(self.print_message)

注意,在设置连接到列表的最后一个线程(即您新添加的线程)之前,必须先断开所有插槽的按钮clicked()信号。这是因为如果你启动了多个线程,clicked()信号将连接到它们所有的插槽上。因此,stop_process_and_thread和stop_thread将根据运行线程的数量被调用多次,并且顺序无法预测。
仅通过断开信号来终止并从列表中删除最后启动的线程。同时,请确保设置setTerminationEnabled。
但是在那之后,您需要重新连接按钮的clicked信号,因此您还需要更改您的stop_thread方法。
def stop_thread(self):
    del(self.new_thread[-1])
    if self.new_thread: # if no threads left, then no need ro reconnect signal
         self.button_stop_thread.clicked.connect(self.new_thread[-1].stop_process_and_thread)
    print self.new_thread 

关于终止,可以在stop_process_and_thread方法内完成。 (这是您已注释的行,在发出信号之前请将其放置在该行之前)

现在您的程序应该按照您的预期运行

对于您的第一个问题,我不确定线程是否被删除,但现在它们肯定被终止了。在以前的代码中,您通过将所有内容分配给一个变量而失去了引用,而其中一些仍然处于活动状态。这也回答了您的第二个问题

至于第三个问题,不建议使用qt文档中的此种方式终止线程。您应该在那里搜索更好且更安全的方法,我也不太确定您使用多进程的方式,但我无法告诉您更多,因为我没有经常使用此模块。

我认为您应该改变编程策略。最好在进程内使用线程,而不是在线程内使用进程,就像您所做的那样。


感谢您的回答!我已经更新了我的问题中的代码:(1) complex_processing() 执行某些操作,它是一个最小示例;(2)self.stop_thread_signal.emit() 不在 if ... 之下。 - Igor
我在我的代码中添加了 setTerminationEnabled(True),但它不起作用。我可以在没有 setTerminationEnabled(True) 的情况下终止线程,这不是问题,但我无法删除线程对象 - 这才是问题!我知道我可以手动删除信号连接。我不想手动删除,我想删除线程对象,然后信号连接也必须被删除。但是它不起作用。 - Igor
你尝试过像我建议的那样将线程对象存储在列表中吗?因为当我这样做时,它们似乎被删除并自动断开连接(正如文档中所述),因此它们不会接收任何信号。手动断开连接不是在要删除的线程上。在我的看法中,问题似乎出现在将不同的线程对象存储在同一变量中。 - finmor
为了更好地理解为什么应该使用列表,请仔细查看https://dev59.com/LXNA5IYBdhLWcg3wYMt-,特别是第二个答案。尽管您删除了new_thread变量,但之前分配给它的对象可能仍然存在。 new_thread只是不再引用它们,但它们仍然存在。 - finmor
谢谢!抱歉,但是你没有理解我的意思。我说的是同一时间只有一个活动线程。而你在谈论并行运行的多个线程。那不是我的问题,伙计。我上面提供了一个最小化的例子——那不是完整的程序。我再说一遍:如果我启动、停止和删除一个线程,按钮信号不会自动断开连接——这就是我的问题。 - Igor
显示剩余5条评论

0

我以前处理过这个问题,并且解决方法是这样的:

import time
from threading import Thread

class myClass(object):
    def __init__(self):
        self._is_exiting = False

        class dummpyClass(object):
            pass
        newSelf = dummpyClass()
        newSelf.__dict__ = self.__dict__

        self._worker_thread = Thread(target=self._worker, args=(newSelf,), daemon=True)

    @classmethod
    def _worker(cls, self):
        i = 0
        while not self._is_exiting:
            print('Loop #{}'.format(i))
            i += 1
            time.sleep(3)
    def __del__(self):
        self._is_exiting = True
        self._worker_thread.join()

使用 dummpyClass 允许工作线程访问我们主类的所有属性,但它实际上并不指向它。因此,当我们删除主类时,它有可能被完全解除引用。
test = myClass()
test._worker_thread.start()

请注意,我们的工作程序开始打印输出。
del test

工作线程已停止。

更复杂的解决这个问题的方法,它将允许您访问 myClass 方法的方式是首先定义一个基类(称之为 myClass_base),该基类存储所有数据和所需的方法,但不支持线程。然后在一个新类中添加线程支持。

import time
from threading import Thread
class myClass_base(object):
    def __init__(self, specialValue):
        self.value = specialValue
    def myFun(self, x):
        return self.value + x

class myClass(myClass_base):
    def __init__(self, *args, **kwargs):

        super(myClass, self).__init__(*args, **kwargs)

        newSelf = myClass_base.__new__(myClass_base)
        newSelf.__dict__ = self.__dict__

        self._is_exiting = False

        self.work_to_do = []

        self._worker_thread = Thread(target=self._worker, args=(newSelf,), daemon=True)

    @classmethod
    def _worker(cls, self):
        i = 0
        while not self._is_exiting:
            if len(self.work_to_do) > 0:
                x = self.work_to_do.pop()
                print('Processing {}.  Result = {}'.format(x, self.myFun(x)))
            else:
                print('Loop #{}, no work to do!'.format(i))
            i += 1
            time.sleep(2)

    def __del__(self):
        self._is_exiting = True
        self._worker_thread.join()

执行以下命令以开始这个类:

test = myClass(3)
test.work_to_do+=[1,2,3,4]
test._worker_thread.start()

请注意,结果开始打印。删除操作与以前相同。执行

del test

并且线程能够正确退出。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接