繁体   English   中英

如何在不阻塞流的情况下保存数据流中的数据? (PyQt5信号emit()性能)

[英]How to save data from the data stream while not blocking the stream? (PyQt5 signal emit() performance)

我正在开发一个PyQt5应用程序。 在我的应用程序中,它有一个数据流,其速度约为5~20个数据/秒。

每次数据到达时,都会调用类Analyzer的以下onData()方法。 (以下代码是我的应用程序的简化代码)

class Analyzer():
    def __init__(self):
        self.cnt = 0
        self.dataDeque = deque(MAXLENGTH=10000)
    def onData(self, data):
        self.dataDeque.append({
            "data": data, 
            "createdTime": time.time()
        })
        self.cnt += 1
        if self.cnt % 10000 == 0:
            pickle.dump(dataDeque, open(file, 'wb'))

但问题是,这个dataDeque对象是如此之大(50~150MB),因此倾倒pickle大约需要1~2秒。

在那一刻(1~2秒),调用onData()方法的请求排队,并且在1~2秒后,排队的请求同时调用大量的onData()方法,最终扭曲createdTime的数据时间。

为了解决这个问题,我编辑了我的代码以使用Thread(QThread)来保存pickle。

以下代码是已编辑的代码。

from PickleDumpingThread import PickleDumpingThread
pickleDumpingThread = PickleDumpingThread()
pickleDumpingThread.start()

class Analyzer():
    def __init__(self):
        self.cnt = 0
        self.dataDeque = deque(MAXLENGTH=10000)
    def onData(self, data):
        self.dataDeque.append({
            "data": data, 
            "createdTime": time.time()
        })
        self.cnt += 1
        if self.cnt % 10000 == 0:
            pickleDumpingThread.pickleDumpingSignal.emit({
                "action": savePickle,
                "deque": self.dataDeque
            })
            # pickle.dump(dataDeque, open(file, 'wb'))

以下代码是PickleDumpingThread类。

class PickleDumpingThread(QThread):
   def __init__(self):
       super().__init__()
       self.daemon = True
       self.pickleDumpingSignal[dict].connect(self.savePickle)

   def savePickle(self, signal_dict):
       pickle.dump(signal_dict["deque"], open(file, 'wb'))

我预计这个新编辑的代码将大大减少流阻塞时间(1~2秒),但是这段代码仍会阻塞流约0.5~2秒。

似乎pickleDumpingThread.pickleDumpingSignal.emit(somedict)需要0.5~2秒。

我的问题是3件事。

  1. 信号emit()函数的性能是不是很好?

  2. 在我的情况下是否有任何替代的emit()函数?

  3. 或者有没有办法在不阻塞数据流的同时保存pickle? (任何修改我的代码的建议都非常感谢)

感谢您阅读这个长期的问题!

这样的事可能有用

class PickleDumpingThread(QThread):
   def __init__(self, data):
       super().__init__()
       self.data = data

   def run(self):
       pickle.dump(self.data["deque"], open(file, 'wb'))
       self.emit(QtCore.SIGNAL('threadFinished(int)'), self.currentThreadId())

class Analyzer():
    def __init__(self):
        self.cnt = 0
        self.dataDeque = deque(MAXLENGTH=10000)
        self.threadHandler = {}

    def onData(self, data):
        self.dataDeque.append({ "data": data, "createdTime": time.time() })
        self.cnt += 1
        if self.cnt % 10000 == 0:
            thread = PickleDumpingThread(self.dataDeque)
            self.connect(thread, QtCore.SIGNAL("threadFinished(int)"), self.threadFinished)
            thread.start() 
            self.threadHandler[thread.currentThreadId()] = thread

    @QtCore.pyqtSlot(int)
    def threadFinished(id):
        del self.threadHandler[id]

self.threadHandler只知道还有多少线程在运行,你可以摆脱它和threadFinished方法

问题是我没有正确使用QThread

印刷的结果

print("(Current Thread)", QThread.currentThread(),"\n")
print("(Current Thread)", int(QThread.currentThreadId()),"\n")

注意到我创建的PickleDumpingThread是在主线程中运行的,而不是在某个单独的线程中运行。

原因是run()QThread中唯一一个在单独线程中运行的函数,所以QThread中的savePickle方法在主线程中运行。


第一解决方案

使用信号的正确用法是使用Worker如下。

from PyQt5.QtCore import QThread
class GenericThread(QThread):
    def run(self, *args):
       #  print("Current Thread: (GenericThread)", QThread.currentThread(),"\n")
        self.exec_()

class PickleDumpingWorker(QObject):
    pickleDumpingSignal = pyqtSignal(dict)
    def __init__(self):
        super().__init__()
        self.pickleDumpingSignal[dict].connect(self.savePickle)

    def savePickle(self, signal_dict)
        pickle.dump(signal_dict["deque"], open(file, "wb"))

pickleDumpingThread = GenericThread()
pickleDumpingThread.start()

pickleDumpingWorker = PickleDumpingWorker()
pickleDumpingWorker.moveToThread(pickleDumpingThread)

class Analyzer():
    def __init__(self):
        self.cnt = 0
        self.dataDeque = deque(MAXLENGTH=10000)
    def onData(self, data):
        self.dataDeque.append({
            "data": data, 
            "createdTime": time.time()
        })
        self.cnt += 1
        if self.cnt % 10000 == 0:
            pickleDumpingWorker.pickleDumpingSignal.emit({
                "action": savePickle,
                "deque": self.dataDeque
            })
            # pickle.dump(dataDeque, open(file, 'wb'))

这个解决方案有效(pickle被拆分成单独的线程),但缺点是数据流由于信号emit()函数仍然延迟大约0.5~1秒。

我发现我的案例的最佳解决方案是@PYPL的代码,但代码需要一些修改才能工作。


最终解决方案

最终解决方案是修改@PYPL的以下代码

thread = PickleDumpingThread(self.dataDeque)
thread.start() 

self.thread = PickleDumpingThread(self.dataDeque)
self.thread.start() 

原始代码有一些运行时错误。 似乎线程在转储pickle之前被垃圾收集,因为在onData()函数完成后没有对该线程的引用。

通过添加self.thread引用该线程解决了这个问题。

此外,似乎老PickleDumpingThread之后新收集被垃圾PickleDumpingThread正在被引用self.thread (因为老PickleDumpingThread失去其参考)。

但是,此声明未经验证(因为我不知道如何查看当前活动的线程)..

无论如何,这个解决方案解决了这个问题。


编辑

我的最终解决方案也有延迟。 调用Thread.start()需要一些时间。

我选择的真正的最终解决方案是在线程中运行无限循环并监视该线程的一些变量以确定何时保存pickle。 在线程中使用无限循环需要大量的cpu,所以我添加了time.sleep(0.1)来减少cpu的使用。


最终编辑

好吧。我的'真正的最终解决方案'也有延迟..即使我将倾销工作转移到另一个QThread,主线程仍然有关于泡菜倾销时间的延迟! 那很奇怪。

但我找到了原因。 原因既不是发射()性能也不是我想的。

原因是,令人尴尬的是, python的Global Interpreter Lock会阻止同一进程中的两个线程同时运行Python代码

所以我可能应该在这种情况下使用多处理模块。

我会在修改代码后发布结果以使用多处理模块。

使用multiprocessing模块和将来的尝试后编辑

使用multiprocessing模块

使用multiprocessing模块解决了并发运行python代码的问题,但出现了新的基本问题。 新问题是'在进程之间传递共享内存变量需要相当长的时间'(在我的情况下,将deque对象传递给子进程需要1~2秒)。 我发现只要我使用multiprocessing模块就无法解决这个问题。 所以我放弃了使用`多处理模块

可能的未来尝试

1.在QThread仅执行文件I / O.

pickle dumping的基本问题不是写入文件,而是在写入文件之前进行序列化。 Python在写入文件时会释放GIL,因此磁盘I / O可以在QThread同时完成。 问题是,在pickle.dump方法中写入文件之前将deque对象序列化为字符串需要花费一些时间,在此期间,主线程将因GIL而被阻塞。

因此,以下方法将有效地减少延迟的长度。

  1. 每次调用onData()时,我们都会以某种方式对数据对象进行字符串化,并将其推送到deque对象

  2. PickleDumpingThread ,只需join list(deque)对象来对deque对象进行字符串化。

  3. file.write(stringified_deque_object) 这可以同时完成。

步骤1需要很短的时间,因此它几乎不会阻塞主线程。 第2步可能需要一些时间,但显然比在pickle.dump方法中序列化python对象花费的时间更短。 步骤3不阻止主线程。

2.使用C扩展名

我们可以手动释放GIL并在我们的自定义C扩展模块中重新获取GIL。 但这可能很脏。

3.将CPython移植到Jython或IronPython

Jython和IronPython是分别使用Java和C#的其他python实现。 因此,他们在实现中不使用GIL,这意味着thread确实像线程一样工作。 一个问题是这些实现不支持PyQt ..

4.移植到另一种语言

..

注意:

  1. json.dump对我的数据也花了1~2秒。

  2. 对于这种情况,Cython不是一个选项。 虽然Cython有with nogil:只能在该块中访问非python对象(无法在该块中访问deque对象),并且我们不能在该块中使用pickle.dump方法。

当GIL出现问题时,解决方法是将任务细分为块,以便您可以在块之间刷新GUI。

例如,假设你有一个庞大的S大小列表要转储,那么你可以尝试定义一个派生自list的类并覆盖getstate返回N个subpickle对象,每个类的一个实例说Subpickle,包含你的S / N项名单。 每个subpickle只在pickle时存在,并定义getstate做两件事:

  • 在gui上调用qApp.processEvents(),和
  • 返回S / N项的子列表。

在unpickling时,每个subpickle将刷新GUI并获取项目列表; 最后,在原始对象中,它将在其setstate中接收的所有子列中重新创建总列表。

如果你想在控制台应用程序(或非pyqt gui)中取消选择pickle,你应该抽象出处理事件的调用。 你可以通过在Subpickle上定义一个类范围的属性,比如process_events,默认为None; 如果不是None,则setstate将其称为函数。 因此,默认情况下,子片段之间没有GUI刷新,除非unpickles的应用程序在unpickling开始之前将此属性设置为可调用。

此策略将使您的GUi有机会在unpickling过程中重绘(如果需要,只有一个线程)。

实现取决于您的确切数据,但这是一个演示大型列表原则的示例:

import pickle

class SubList:
    on_pickling = None

    def __init__(self, sublist):
        print('SubList', sublist)
        self.data = sublist

    def __getstate__(self):
        if SubList.on_pickling is not None:
            print('SubList pickle state fetch: calling sub callback')
            SubList.on_pickling()
        return self.data

    def __setstate__(self, obj):
        if SubList.on_pickling is not None:
            print('SubList pickle state restore: calling sub callback')
            SubList.on_pickling()
        self.data = obj


class ListSubPickler:
    def __init__(self, data: list):
        self.data = data

    def __getstate__(self):
        print('creating SubLists for pickling long list')
        num_chunks = 10
        span = int(len(self.data) / num_chunks)
        SubLists = [SubList(self.data[i:(i + span)]) for i in range(0, len(self.data), span)]
        return SubLists

    def __setstate__(self, subpickles):
        self.data = []
        print('restoring Pickleable(list)')
        for subpickle in subpickles:
            self.data.extend(subpickle.data)
        print('final', self.data)

def refresh():
    # do something: refresh GUI (for example, qApp.processEvents() for Qt), show progress, etc
    print('refreshed')

data = list(range(100))  # your large data object
list_pickler = ListSubPickler(data)
SubList.on_pickling = refresh

print('\ndumping pickle of', list_pickler)
pickled = pickle.dumps(list_pickler)

print('\nloading from pickle')
new_list_pickler = pickle.loads(pickled)
assert new_list_pickler.data == data

print('\nloading from pickle, without on_pickling')
SubList.on_pickling = None
new_list_pickler = pickle.loads(pickled)
assert new_list_pickler.data == data

易于应用于dict,甚至可以使其适应使用isinstance接收的数据类型。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM