Обновление меток в отдельном работнике (экземпляр процесса)

У меня есть несколько экранов. Один из них (DataScreen) содержит 8 меток, которые должны показывать текущие значения датчика. Датчики считываются отдельным процессом (который запускается с MainScreen). Сам процесс является экземпляром multiprocessing.Process.

Я могу получить ссылку на метки sensor_labels = self.manager.get_screen('data').l

Однако я не могу понять, как изменить их в подпроцессе. Я могу изменить их из любой функции, которая не является отдельным процессом, просто сделав что-то вроде:

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

К сожалению, сложнее передать ссылку sensor_labels работнику. Если я передам их как аргумент, то оба процесса (kivy и рабочий), похоже, используют один и тот же объект (id одинаковый). Однако, если я изменю label.text = 'New Text', ничего не изменится в Kivy.

Почему идентификатор обоих объектов одинаковый, но текст не изменяется? И как я могу совместно использовать объект метки 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'

Ответ 1

Похоже, вы можете неправильно понять, как работает многопроцессорность.

Когда вы начинаете новый Process с помощью библиотеки многопроцессорности, он создает новый процесс и рассосает весь код, необходимый для запуска целевой функции. Любые обновления, которые вы делаете для пропущенных меток, происходят в рабочем процессе и НЕ отразятся в процессе пользовательского интерфейса.

Чтобы обойти это, вы должны использовать один из этих методов для обмена данными между рабочими и процессами пользовательского интерфейса: https://docs.python.org/2/library/multiprocessing.html#exchanging-objects-between-processes. Поскольку у вас уже есть очередь, вы можете сделать что-то вроде этого:

Поместите ваш read_sensors в worker.py, передавая tx и rx Queue, где tx используется для отправки в пользовательский интерфейс, а rx используется для чтения из пользовательского интерфейса.

#! /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 для обновлений. При выходе из пользовательского интерфейса пользователь может прекратить работу, помещая сообщение в очередь rx.

#! /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 уже является "процессом" безопасным, вам не нужно использовать блокировку вокруг него. Если у вас есть отдельный процесс для каждого датчика, вы можете использовать ту же идею, что и больше очередей.

Ответ 2

Kivy не предоставляет IPC, а элементы GUI должны обновляться только в основном потоке. Для реализации IPC вы можете использовать OSC для облегчения этого, см. this. Если вы переместите считывание датчиков внутри потоков, прочитайте this и this, если вы еще этого не сделали.