signal-slot:python版本的多进程通信的信号与槽机制(编程模式)的库(library) —— 强化学习ppo算法库sample-factory的多进程包装器,实现类似Qt的多进程编程模式(信号与槽机制) —— python3.12版本下成功通过测试
什么是 Qt
signal-slot库项目地址:
https://github.com/alex-petrenko/signal-slot
该库实现的主要原理:
要注意这个项目的library只是对原生的信号与槽机制进行了一定程度的复现,因此和原生的实现还有较大的差距,这个项目主要是项目作者为了给自己写“强化学习算法库sample-factory”提供并行编程模式上的支持而写,因此可以看做是一个临时项目或者说是一个科研辅助类的项目,并不具有工程实用性,换句话说,这个项目就是论文代码的一部分(辅助代码),有些toy的意味。
该项目中大量的逻辑问题和冗余代码,很多地方也都有bug存在,但是这个项目当初创建时是因为原作者在写“强化学习并行算法库”时感觉并行编程过于繁琐,因此编写了这个库来实现更便捷的python并行编程模式。
该项目的信号与槽机制只实现了下面几个功能:
一个信号可以连接多个槽:
当信号发射时,会以不确定的顺序一个接一个的调用各个槽。多个信号可以连接同一个槽
即无论是哪一个信号被发射,都会调用这个槽。
- 信号直接可以相互连接
发射第一个信号时,也会发射第二个信号。
- 连接可以被移除
这种情况用得比较少,因为在对象被删除时,Qt会自动移除与这个对象相关的所有连接。
并且该库只实现了进程间的通信,并且是主进程与子进程间的通信,并不支持子进程之间的直接通信。
主要代码:
from __future__ import annotations
import logging
import multiprocessing
import os
import time
import types
import uuid
from dataclasses import dataclass
from queue import Empty, Full
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
from signal_slot.queue_utils import get_queue
from signal_slot.utils import error_log_every_n
log = logging.getLogger(__name__)
def configure_logger(logger):
global log
log = logger
# type aliases for clarity
ObjectID = Any # ObjectID can be any hashable type, usually a string
MpQueue = Any # can actually be any multiprocessing Queue type, i.e. faster_fifo queue
BoundMethod = Any
StatusCode = int
@dataclass(frozen=True)
class Emitter:
object_id: ObjectID
signal_name: str
@dataclass(frozen=True)
class Receiver:
object_id: ObjectID
slot_name: str
# noinspection PyPep8Naming
class signal:
def __init__(self, _):
self._name = None
self._obj: Optional[EventLoopObject] = None
@property
def obj(self):
return self._obj
@property
def name(self):
return self._name
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None):
assert isinstance(
obj, EventLoopObject
), f"signals can only be added to {EventLoopObject.__name__}, not {type(obj)}"
self._obj = obj
return self
def connect(self, other: Union[EventLoopObject, BoundMethod], slot: str = None):
self._obj.connect(self._name, other, slot)
def disconnect(self, other: Union[EventLoopObject, BoundMethod], slot: str = None):
self._obj.disconnect(self._name, other, slot)
def emit(self, *args):
self._obj.emit(self._name, *args)
def emit_many(self, list_of_args: Iterable[Tuple]):
self._obj.emit_many(self._name, list_of_args)
def broadcast_on(self, event_loop: EventLoop):
self._obj.register_broadcast(self._name, event_loop)
class EventLoopObject:
def __init__(self, event_loop, object_id=None):
# the reason we can't use regular id() is because depending on the process spawn method the same objects
# can have the same id() (in Fork method) or different id() (spawn method)
self.object_id = object_id if object_id is not None else self._default_obj_id()
# check if there is already an object with the same id on this event loop
if self.object_id in event_loop.objects:
raise ValueError(f"{self.object_id=} is already registered on {event_loop=}")
self.event_loop: EventLoop = event_loop
self.event_loop.objects[self.object_id] = self
# receivers of signals emitted by this object
self.send_signals_to: Dict[str, Set[ObjectID]] = dict()
self.receiver_queues: Dict[ObjectID, MpQueue] = dict()
self.receiver_refcount: Dict[ObjectID, int] = dict()
# connections (emitter -> slot name)
self.connections: Dict[Emitter, str] = dict()
def _default_obj_id(self):
return str(uuid.uuid4())
def _add_to_loop(self, loop):
self.event_loop = loop
self.event_loop.objects[self.object_id] = self
@staticmethod
def _add_to_dict_of_sets(d: Dict[Any, Set], key, value):
if key not in d:
d[key] = set()
d[key].add(value)
@staticmethod
def _throw_if_different_processes(o1: EventLoopObject, o2: EventLoopObject):
o1_p, o2_p = o1.event_loop.process, o2.event_loop.process
if o1_p != o2_p:
msg = f"Objects {o1.object_id} and {o2.object_id} live on different processes"
log.error(msg)
raise RuntimeError(msg)
@staticmethod
def _bound_method_to_obj_slot(obj, slot):
if isinstance(obj, (types.MethodType, types.BuiltinMethodType)):
slot = obj.__name__
obj = obj.__self__
assert isinstance(obj, EventLoopObject), f"slot should be a method of {EventLoopObject.__name__}"
assert slot is not None
return obj, slot
def connect(self, signal_: str, other: EventLoopObject | BoundMethod, slot: str = None):
other, slot = self._bound_method_to_obj_slot(other, slot)
self._throw_if_different_processes(self, other)
emitter = Emitter(self.object_id, signal_)
receiver_id = other.object_id
# check if we already have a different object with the same name
if receiver_id in self.event_loop.objects:
if self.event_loop.objects[receiver_id] is not other:
raise ValueError(f"{receiver_id=} object is already registered on {self.event_loop.object_id=}")
self._add_to_dict_of_sets(self.send_signals_to, signal_, receiver_id)
receiving_loop = other.event_loop
self._add_to_dict_of_sets(receiving_loop.receivers, emitter, receiver_id)
q = receiving_loop.signal_queue
self.receiver_queues[receiver_id] = q
self.receiver_refcount[receiver_id] = self.receiver_refcount.get(receiver_id, 0) + 1
other.connections[emitter] = slot
def disconnect(self, signal_, other: EventLoopObject | BoundMethod, slot: str = None):
other, slot = self._bound_method_to_obj_slot(other, slot)
self._throw_if_different_processes(self, other)
if signal_ not in self.send_signals_to:
log.warning(f"{self.object_id}:{signal_=} is not connected to anything")
return
receiver_id = other.object_id
if receiver_id not in self.send_signals_to[signal_]:
log.warning(f"{self.object_id}:{signal_=} is not connected to {receiver_id}:{slot=}")
return
self.send_signals_to[signal_].remove(receiver_id)
self.receiver_refcount[receiver_id] -= 1
if self.receiver_refcount[receiver_id] <= 0:
del self.receiver_refcount[receiver_id]
del self.receiver_queues[receiver_id]
emitter = Emitter(self.object_id, signal_)
del other.connections[emitter]
loop_receivers = other.event_loop.receivers.get(emitter)
if loop_receivers is not None:
loop_receivers.remove(other.object_id)
def register_broadcast(self, signal_: str, event_loop: EventLoop):
self.connect(signal_, event_loop.broadcast)
def subscribe(self, signal_: str, slot: Union[BoundMethod, str]):
if isinstance(slot, (types.MethodType, types.BuiltinMethodType)):
slot = slot.__name__
self.event_loop.connect(signal_, self, slot)
def unsubscribe(self, signal_: str, slot: Union[BoundMethod, str]):
if isinstance(slot, (types.MethodType, types.BuiltinMethodType)):
slot = slot.__name__
self.event_loop.disconnect(signal_, self, slot)
def emit(self, signal_: str, *args):
self.emit_many(signal_, (args,))
def emit_many(self, signal_: str, list_of_args: Iterable[Tuple]):
# enable for debugging
# pid = process_pid(self.event_loop.process)
# if os.getpid() != pid:
# raise RuntimeError(
# f'Cannot emit {signal_}: object {self.object_id} lives on a different process {pid}!'
# )
# this is too verbose for most situations
# if self.event_loop.verbose:
# log.debug(f"Emit {self.object_id}:{signal_=} {list_of_args=}")
signals_to_emit = tuple((self.object_id, signal_, args) for args in list_of_args)
# find a set of queues we need to send this signal to
receiver_ids = self.send_signals_to.get(signal_, ())
queues = set()
for receiver_id in receiver_ids:
queues.add(self.receiver_queues[receiver_id])
for q in queues:
# we just push messages into each receiver event loop queue
# event loops themselves will redistribute the signals to all receivers living on that loop
try:
q.put_many(signals_to_emit, block=False)
except Full as exc:
receivers = sorted([r_id for r_id in receiver_ids if self.receiver_queues[r_id] is q])
error_log_every_n(log, 100, f"{self.object_id}:{signal_=} queue is Full ({exc}). {receivers=}")
def detach(self):
"""Detach the object from it's current event loop."""
if self.event_loop:
del self.event_loop.objects[self.object_id]
self.event_loop = None
def __del__(self):
self.detach()
class EventLoopStatus:
NORMAL_TERMINATION, INTERRUPTED = range(2)
class EventLoop(EventLoopObject):
def __init__(self, unique_loop_name, serial_mode=False):
# objects living on this loop
self.objects: Dict[ObjectID, EventLoopObject] = dict()
super().__init__(self, unique_loop_name)
# object responsible for stopping the loop (if any)
self.owner: Optional[EventLoopObject] = None
# here None means we're running on the main process, otherwise it is the process we belong to
self.process: Optional[EventLoopProcess] = None
self.signal_queue = get_queue(serial=serial_mode, buffer_size_bytes=5_000_000)
# Separate container to keep track of timers living on this thread. Start with one default timer.
self.timers: List[Timer] = []
self.default_timer = Timer(self, 0.05, object_id=f"{self.object_id}_timer")
self.receivers: Dict[Emitter, Set[ObjectID]] = dict()
# emitter of the signal which is currently being processed
self.curr_emitter: Optional[Emitter] = None
self.should_terminate = False
self.verbose = False
# connect to our own termination signal
self._internal_terminate.connect(self._terminate)
@signal
def start(self):
"""Emitted right before the start of the loop."""
...
@signal
def terminate(self):
"""Emitted upon loop termination."""
...
@signal
def _internal_terminate(self):
"""Internal signal: do not connect to this."""
...
def add_timer(self, t: Timer):
self.timers.append(t)
def remove_timer(self, t: Timer):
self.timers.remove(t)
def stop(self):
"""
Graceful termination: the loop will process all unprocessed signals before exiting.
After this the loop does only one last iteration, if any new signals are emitted during this last iteration
they will be ignored.
"""
self._internal_terminate.emit()
def _terminate(self):
"""Forceful termination, some of the signals currently in the queue might remain unprocessed."""
self.should_terminate = True
def broadcast(self, *args):
curr_signal = self.curr_emitter.signal_name
# we could re-emit the signal to reuse the existing signal propagation mechanism, but we can avoid
# doing this to reduce overhead
self._process_signal((self.object_id, curr_signal, args))
def _process_signal(self, signal_):
if self.verbose:
log.debug(f"{self} received {signal_=}...")
emitter_object_id, signal_name, args = signal_
emitter = Emitter(emitter_object_id, signal_name)
receiver_ids = tuple(self.receivers.get(emitter, ()))
for obj_id in receiver_ids:
obj = self.objects.get(obj_id)
if obj is None:
if self.verbose:
log.warning(
f"{self} attempting to call a slot on an object {obj_id} which is not found on this loop ({signal_=})"
)
self.receivers[emitter].remove(obj_id)
continue
slot = obj.connections.get(emitter)
if obj is None:
log.warning(f"{self} {emitter=} does not appear to be connected to {obj_id=}")
continue
if not hasattr(obj, slot):
log.warning(f"{self} {slot=} not found in object {obj_id}")
continue
slot_callable = getattr(obj, slot)
if not isinstance(slot_callable, Callable):
log.warning(f"{self} {slot=} of {obj_id=} is not callable")
continue
self.curr_emitter = emitter
if self.verbose:
log.debug(f"{self} calling slot {obj_id}:{slot}")
# noinspection PyBroadException
try:
slot_callable(*args)
except Exception as exc:
log.exception(f"{self} unhandled exception in {slot=} connected to {emitter=}, {args=}")
raise exc
def _calculate_timeout(self) -> Timer:
# This can potentially be replaced with a sorted set of timers to optimize this linear search for the
# closest timer.
closest_timer = min(self.timers, key=lambda t: t.next_timeout())
return closest_timer
def _loop_iteration(self) -> bool:
closest_timer = self._calculate_timeout()
try:
# loop over all incoming signals, see if any of the objects living on this event loop are connected
# to this particular signal, call slots if needed
signals = self.signal_queue.get_many(timeout=closest_timer.remaining_time())
except Empty:
signals = ()
finally:
if closest_timer.remaining_time() <= 0:
# this is inefficient if we have a lot of short timers, but should do for now
for t in self.timers:
if t.remaining_time() <= 0:
t.fire()
for s in signals:
self._process_signal(s)
if self.should_terminate:
log.debug(f"Loop {self.object_id} terminating...")
self.terminate.emit()
return False
return True
def exec(self) -> StatusCode:
status: StatusCode = EventLoopStatus.NORMAL_TERMINATION
self.default_timer.start() # this will add timer to the loop's list of timers
try:
self.start.emit()
while self._loop_iteration():
pass
except Exception as exc:
log.warning(f"Unhandled exception {exc} in evt loop {self.object_id}")
raise exc
except KeyboardInterrupt:
log.info(f"Keyboard interrupt detected in the event loop {self}, exiting...")
status = EventLoopStatus.INTERRUPTED
return status
def process_events(self):
self._loop_iteration()
def __str__(self):
return f"EvtLoop [{self.object_id}, process={process_name(self.process)}]"
class Timer(EventLoopObject):
def __init__(self, event_loop: EventLoop, interval_sec: float, single_shot=False, object_id=None):
super().__init__(event_loop, object_id)
self._interval_sec = interval_sec
self._single_shot = single_shot
self._is_active = False
self._next_timeout = None
self.start()
@signal
def timeout(self):
pass
def set_interval(self, interval_sec: float):
self._interval_sec = interval_sec
if self._is_active:
self._next_timeout = min(self._next_timeout, time.time() + self._interval_sec)
def stop(self):
if self._is_active:
self._is_active = False
self.event_loop.remove_timer(self)
self._next_timeout = time.time() + 1e10
def start(self):
if not self._is_active:
self._is_active = True
self.event_loop.add_timer(self)
self._next_timeout = time.time() + self._interval_sec
def _emit(self):
self.timeout.emit()
def fire(self):
self._emit()
if self._single_shot:
self.stop()
else:
self._next_timeout += self._interval_sec
def next_timeout(self) -> float:
return self._next_timeout
def remaining_time(self) -> float:
return max(0, self._next_timeout - time.time())
def _default_obj_id(self):
return f"{Timer.__name__}_{super()._default_obj_id()}"
class TightLoop(Timer):
def __init__(self, event_loop: EventLoop, object_id=None):
super().__init__(event_loop, 0.0, object_id)
@signal
def iteration(self):
pass
def _emit(self):
self.iteration.emit()
class EventLoopProcess(EventLoopObject):
def __init__(
self, unique_process_name, multiprocessing_context=None, init_func=None, args=(), kwargs=None, daemon=None
):
"""
Here we could've inherited from Process, but the actual class of process (i.e. Process vs SpawnProcess)
depends on the multiprocessing context and hence is not known during the generation of the class.
Instead of using inheritance we just wrap a process instance.
"""
process_cls = multiprocessing.Process if multiprocessing_context is None else multiprocessing_context.Process
self._process = process_cls(target=self._target, name=unique_process_name, daemon=daemon)
self._init_func: Optional[Callable] = init_func
self._args = self._kwargs = None
self.set_init_func_args(args, kwargs)
self.event_loop = EventLoop(f"{unique_process_name}_evt_loop")
EventLoopObject.__init__(self, self.event_loop, unique_process_name)
def set_init_func_args(self, args=(), kwargs=None):
assert not self._process.is_alive()
self._args = tuple(args)
self._kwargs = dict() if kwargs is None else dict(kwargs)
def _target(self):
if self._init_func:
self._init_func(*self._args, **self._kwargs)
self.event_loop.exec()
def start(self):
self.event_loop.process = self
self._process.start()
def stop(self):
self.event_loop.stop()
def terminate(self):
self._process.terminate()
def kill(self):
self._process.kill()
def join(self, timeout=None):
self._process.join(timeout)
def is_alive(self):
return self._process.is_alive()
def close(self):
return self._process.close()
@property
def name(self):
return self._process.name
@property
def daemon(self):
return self._process.daemon
@property
def exitcode(self):
return self._process.exitcode
@property
def ident(self):
return self._process.ident
pid = ident
def process_name(p: Optional[EventLoopProcess]):
if p is None:
return f"main process {os.getpid()}"
elif isinstance(p, EventLoopProcess):
return p.name
else:
raise RuntimeError(f"Unknown process type {type(p)}")
def process_pid(p: Optional[EventLoopProcess]):
if p is None:
return os.getpid()
elif isinstance(p, EventLoopProcess):
# noinspection PyProtectedMember
return p._process.pid
else:
raise RuntimeError(f"Unknown process type {type(p)}")
signal-slot:python版本的多进程通信的信号与槽机制(编程模式)的库(library) —— 强化学习ppo算法库sample-factory的多进程包装器,实现类似Qt的多进程编程模式(信号与槽机制) —— python3.12版本下成功通过测试的更多相关文章
- Python中根据库包名学习使用该库包
目录 Python库包模块 import 语句 from-import 语句 搜索路径 PYTHONPATH 变量 命名空间和作用域 查看模块中所有变量和函数,以及查看具体函数的用法 globals( ...
- python类库32[多进程通信Queue+Pipe+Value+Array]
多进程通信 queue和pipe的区别: pipe用来在两个进程间通信.queue用来在多个进程间实现通信. 此两种方法为所有系统多进程通信的基本方法,几乎所有的语言都支持此两种方法. 1)Queue ...
- QT写hello world 以及信号槽机制
QT是一个C++的库,不仅仅有GUI的库.首先写一个hello world吧.敲代码,从hello world 写起. #include<QtGui/QApplication> #incl ...
- 详解 Qt 线程间共享数据(使用signal/slot传递数据,线程间传递信号会立刻返回,但也可通过connect改变)
使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享 ...
- python改成了python3的版本,那么这时候yum就出问题了
既然把默认python改成了python3的版本,那么这时候yum就出问题了,因为yum貌似不支持python3,开发了这个命令的老哥也不打算继续写支持python3的版本了,所以,如果和python ...
- Qt信号槽的一些事 Qt::带返回值的信号发射方式
一般来说,我们发出信号使用emit这个关键字来操作,但是会发现,emit并不算一个调用,所以它没有返回值.那么如果我们发出这个信号想获取一个返回值怎么办呢? 两个办法:1.通过出参形式返回,引用或者指 ...
- 使用 C++11 编写类似 QT 的信号槽——上篇
了解 QT 的应该知道,QT 有一个信号槽 Singla-Slot 这样的东西.信号槽是 QT 的核心机制,用来替代函数指针,将不相关的对象绑定在一起,实现对象间的通信. 考虑为 Simple2D 添 ...
- QT窗体间传值总结之Signal&Slot
在写程序时,难免会碰到多窗体之间进行传值的问题.依照自己的理解,我把多窗体传值的可以使用的方法归纳如下: 1.使用QT中的Signal&Slot机制进行传值: 2.使用全局变量: 3.使用pu ...
- QT 中 关键字讲解(emit,signal,slot)
Qt中的类库有接近一半是从基类QObject上继承下来,信号与反应槽(signals/slot)机制就是用来在QObject类或其子类间通讯的方法.作为一种通用的处理机制,信号与反应槽非常灵活,可以携 ...
- Python2+python3——多版本启动和多版本pip install问题
背景描述: python2版本都知道维护到2020年,目前使用python的很大一部分用户群体都开始改安装并且使用最新版的python3版本了,python2和python3在编程大的层面不曾改变,有 ...
随机推荐
- kettle从入门到精通 第二十七课 邮件发送
1.我们平常在做数据同步的时候,担心转换或者job没有正常运行,需要加上监控机制,这个时候就会用到邮件功能. 下图是一个简单的测试邮件发送功能的转换.在kettle.properties文件中设置邮件 ...
- AT_agc044_c
problem & blog 由于看到和三进制有关的操作,可以想到建造每个结点都有三个儿子的 Trie.考虑维护两种操作. 1.Salasa 舞 对于这种操作,就是把每一个节点的第一个儿子和第 ...
- 网络问题排查必备利器:Pingmesh
背景 当今的数字化世界离不开无处不在的网络连接.无论是日常生活中的社交媒体.电子商务,还是企业级应用程序和云服务,我们对网络的依赖程度越来越高.然而,网络的可靠性和性能往往是一个复杂的问题,尤其是在具 ...
- 如何解决系统报错:nf_conntrack: table full, dropping packets
问题 在系统日志中(/var/log/messages),有时会看到大面积的下面的报错: nf_conntrack: table full, dropping packet 这说明系统接到了大量的连接 ...
- java线程的park unpark方法
标签(空格分隔): 多线程 park 和 unpark的使用 park和unpark并不是线程的方法,而是LockSupport的静态方法 暂停当前线程 LockSupport.park();//所在 ...
- 端口占用,无法通过netstat找到进程,占用的端口又不能修改,该怎么办?
最近遇到一个奇葩的问题,项目跑的好好的,没有安装其它特殊软件,突然服务器启动报错,日志如下,显然是服务器的8080端口占用了. Caused by: java.net.BindException: A ...
- vue3实现模拟地图上,站点名称按需显示的功能
很久很久没有更新博客了,因为实在是太忙了,每天都有公司的事情忙不完....... 最近在做车辆模拟地图,在实现控制站点名称按需显示时,折腾了好一段时间,特此记录一下.最终界面如下图所示: 站点显示需求 ...
- Win10任务栏图标居中
win+q键搜索并打开字符映射表 点击第五行的空白字符,然后先后点击下方的选择以及复制 在桌面新建一个文件夹,然后重命名,将刚才复制的空白字符粘贴进去,如图,这样我们就拥有了一个空白名称的文件夹 在任 ...
- EthernetIP IO从站设备数据 转 Modbus RTU TCP项目案例
1 案例说明 1. 设置网关采集EthernetIP IO设备数据 2. 把采集的数据转成Modbus协议转发给其他系统. 2 VFBOX网关工作原理 VFBOX ...
- java --面试题大全
J2EE面试题 文档版本号:V2.0 2016年11月 目 录 1. Java基础部分 8 1.1. 一个".java"源文 ...