簡體   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