在单独的worker (Process实例)中更新标签



我确实有几个屏幕。其中一个(datasscreen)包含8个标签,应该显示当前的传感器值。传感器由一个单独的进程读取(从主屏幕启动)。进程本身是multiprocessing.Process的一个实例。

我可以通过sensor_labels = self.manager.get_screen('data').l

获得对标签的引用

然而,我不知道如何在子进程中更改它们。我可以从任何不是单独进程的函数中更改它们,只需执行如下操作:

for item in sensor_labels:
    item.text = 'Update'

不幸的是,将sensor_labels的引用传递给worker似乎更加困难。如果我将它们作为参数传递,两个进程(kivy和worker)似乎共享同一个对象(id是相同的)。然而,如果我改变label.text = 'New Text', Kivy没有任何变化。

为什么两个对象的id相同,但文本没有改变?如何与另一个进程共享Kivy标签对象?

这是我的工作最小的例子

#! /usr/bin/env python
""" Reading sensor data
"""
from kivy.config import Config
Config.set('kivy', 'keyboard_mode', 'multi')
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import StringProperty, ObjectProperty, NumericProperty
from kivy.uix.label import Label
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.stacklayout import StackLayout
from multiprocessing import Process, Queue, Array
# all other modules
import time
import numpy as np
from multiprocessing import Lock
class MainScreen(Screen):
    def __init__(self, **kwargs):
        super(MainScreen, self).__init__(**kwargs)
        self.n_probes = 8
    @staticmethod
    def read_sensors(qu_rx, sensor_labels, lock):
        while True:
            if not qu_rx.empty():
                message = qu_rx.get()
                if message == 'STOP':
                    print('Worker: received poison pill')
                    break
            data = np.random.random()
            print('ID of labels in worker: {}'.format(id(sensor_labels)))
            print('Text of labels in worker:')
            lock.acquire()
            for label in sensor_labels:
                label.text = '{0:2f}'.format(data)
                print(label.text)
            lock.release()
            time.sleep(5)
    def run_worker(self, *args, **kwargs):
        self.qu_tx_worker = Queue()
        lock = Lock()
        # this is a reference to the labels in the DataScreen class
        self.sensor_labels = self.manager.get_screen('data').l
        self.worker = Process(target=self.read_sensors,
                              args=(self.qu_tx_worker, self.sensor_labels, lock))
        self.worker.daemon = True
        self.worker.start()
    def stop_worker(self, *args, **kwargs):
        self.qu_tx_worker.put('STOP')
        print('Send poison pill')
        self.worker.join()
        print('All worker dead')
        print('ID of labels in Kivy: {}'.format(id(self.sensor_labels)))
        print('Label text in Kivy:')
        for label in self.sensor_labels:
            print(label.text)

class DataScreen(Screen):
    def __init__(self, **kwargs):
        layout = StackLayout()
        super(DataScreen, self).__init__(**kwargs)
        self.n_probes = 8
        self.label_text = []
        for i in range(self.n_probes):
            self.label_text.append(StringProperty())
            self.label_text[i] = str(i)
        self.l = []
        for i in range(self.n_probes):
            self.l.append(Label(id='l_{}'.format(i),
                          text='Start {}'.format(i),
                          font_size='60sp',
                          height=20,
                          width=20,
                          size_hint=(0.5, 0.2)))
            self.ids.stack.add_widget(self.l[i])
    def change_text(self):
            for item in self.l:
                item.text = 'Update'

Builder.load_file('phapp.kv')
class MyApp(App):
    """
    The settings App is the main app of the pHBot application.
    It is initiated by kivy and contains the functions defining the main interface.
    """
    def build(self):
        """
        This function initializes the app interface and has to be called "build(self)".
        It returns the user interface defined by the Builder.
        """
        sm = ScreenManager()
        sm.add_widget(MainScreen())
        sm.add_widget(DataScreen())
        # returns the user interface defined by the Builder
        return sm
if __name__ == '__main__':
    MyApp().run()

.kv文件:

<MainScreen>:
    name: 'main'
    BoxLayout:
        orientation: 'vertical'
        Button:
            text: 'Start Application'
            font_size: 40
            on_release: root.run_worker()
        Button:
            text: 'Stop Application'
            font_size: 40
            on_release: root.stop_worker()
        Button:
            text: 'Go to data'
            font_size: 40
            on_release: app.root.current = 'data'
        Button:
            text: 'Exit'
            font_size: 40
            on_release: app.stop()
<DataScreen>:
    name: 'data'
    StackLayout:
        id: stack
        orientation: 'lr-tb'
    BoxLayout:
        Button:
            size_hint: (0.5, 0.1)
            text: 'Update'
            font_size: 30
            on_release: root.change_text()
        Button:
            size_hint: (0.5, 0.1)
            text: 'Back to main menu'
            font_size: 30
            on_release: app.root.current = 'main'

看起来您可能误解了多处理的工作原理。

当您使用multiprocessing库启动一个新的Process时,它会创建一个新进程并pickle运行目标函数所需的所有代码。你对传递的标签所做的任何更新都发生在工作进程中,不会反映在UI进程中。

要解决这个问题,您必须使用以下方法之一在工作进程和UI进程之间交换数据:https://docs.python.org/2/library/multiprocessing.html#exchanging-objects-between-processes。既然你已经有了一个队列,你可以这样做:

read_sensors放入worker.py中,传递txrx队列,其中tx用于发送到UI, rx用于从UI读取。

#! /usr/bin/env python
""" Reading sensor data
"""
import time
import numpy as np
def read_sensors(rx,tx, n):
    while True:
        if not rx.empty():
            message = rx.get()
            if message == 'STOP':
                print('Worker: received poison pill')
                break
        #: Sensor value for each label
        data = [np.random.random() for i in range(n)]
        #: Formatted data
        new_labels = ['{0:2f}'.format(x) for x in data]
        print('Text of labels in worker: {}'.format(new_labels))
        #lock.acquire() # Queue is already safe, no need to lock
        #: Put the formatted label in the tx queue
        tx.put(new_labels)
        # lock.release() # Queue is already safe, no need to unlock
        time.sleep(5)

然后在你的应用程序中使用Clock调用更新处理程序来定期检查tx队列的更新。当退出时,UI可以通过在rx队列中放置消息来告诉worker停止。

#! /usr/bin/env python
""" Reading sensor data
"""
from kivy.config import Config
from kivy.clock import Clock
Config.set('kivy', 'keyboard_mode', 'multi')
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import StringProperty, ObjectProperty, NumericProperty
from kivy.uix.label import Label
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.stacklayout import StackLayout
from multiprocessing import Process, Queue
#: Separate worker file so a separate app is not opened
import worker
class MainScreen(Screen):
    def __init__(self, **kwargs):
        super(MainScreen, self).__init__(**kwargs)
        self.n_probes = 8
        #: Hold the update event
        self._event = None
    def read_worker(self,dt):
        """ Read the data from the worker process queue"""
        #: Get the data from the worker (if given) without blocking
        if self.tx.empty():
            return # No data, try again later
        #: The worker put data in the queue, update the labels
        new_labels = self.tx.get()
        for label,text in zip(self.sensor_labels,new_labels):
            label.text = text
    def run_worker(self, *args, **kwargs):
        self.rx = Queue() #: Queue to send data to worker process 
        self.tx = Queue() #: Queue to recv from worker process
        self.sensor_labels = self.manager.get_screen('data').l
        self.worker = Process(target=worker.read_sensors,
                              args=(self.rx,self.tx,self.n_probes))
        self.worker.daemon = True
        self.worker.start()
        # Check the tx queue for updates every 0.5 seconds
        self._event = Clock.schedule_interval(self.read_worker, 0.5)
    def stop_worker(self, *args, **kwargs):
        self.rx.put('STOP')
        print('Send poison pill')
        self.worker.join()
        print('All worker dead')
        #: Stop update loop
        if self._event:
            self._event.cancel()
        print('ID of labels in Kivy: {}'.format(id(self.sensor_labels)))
        print('Label text in Kivy:')
        for label in self.sensor_labels:
            print(label.text)

class DataScreen(Screen):
    def __init__(self, **kwargs):
        layout = StackLayout()
        super(DataScreen, self).__init__(**kwargs)
        self.n_probes = 8
        self.label_text = []
        for i in range(self.n_probes):
            self.label_text.append(StringProperty())
            self.label_text[i] = str(i)
        self.l = []
        for i in range(self.n_probes):
            self.l.append(Label(id='l_{}'.format(i),
                          text='Start {}'.format(i),
                          font_size='60sp',
                          height=20,
                          width=20,
                          size_hint=(0.5, 0.2)))
            self.ids.stack.add_widget(self.l[i])
    def change_text(self):
            for item in self.l:
                item.text = 'Update'

Builder.load_file('phapp.kv')
class MyApp(App):
    """
    The settings App is the main app of the pHBot application.
    It is initiated by kivy and contains the functions defining the main interface.
    """
    def build(self):
        """
        This function initializes the app interface and has to be called "build(self)".
        It returns the user interface defined by the Builder.
        """
        sm = ScreenManager()
        sm.add_widget(MainScreen())
        sm.add_widget(DataScreen())
        # returns the user interface defined by the Builder
        return sm
if __name__ == '__main__':
    MyApp().run()

另外multiprocessing.Queue类已经是"进程"安全的,你不需要在它周围使用锁。如果每个传感器都有单独的进程,则可以使用相同的方法,只是增加队列。

Kivy不提供IPC, GUI元素应该只在主线程中更新。要实现IPC,你可以使用OSC来促进它,参见此。如果您将读取传感器移动到线程中,那么读取这个和这个(如果您还没有这样做)。

最新更新