什么是 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版本下成功通过测试的更多相关文章

  1. Python中根据库包名学习使用该库包

    目录 Python库包模块 import 语句 from-import 语句 搜索路径 PYTHONPATH 变量 命名空间和作用域 查看模块中所有变量和函数,以及查看具体函数的用法 globals( ...

  2. python类库32[多进程通信Queue+Pipe+Value+Array]

    多进程通信 queue和pipe的区别: pipe用来在两个进程间通信.queue用来在多个进程间实现通信. 此两种方法为所有系统多进程通信的基本方法,几乎所有的语言都支持此两种方法. 1)Queue ...

  3. QT写hello world 以及信号槽机制

    QT是一个C++的库,不仅仅有GUI的库.首先写一个hello world吧.敲代码,从hello world 写起. #include<QtGui/QApplication> #incl ...

  4. 详解 Qt 线程间共享数据(使用signal/slot传递数据,线程间传递信号会立刻返回,但也可通过connect改变)

    使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享 ...

  5. python改成了python3的版本,那么这时候yum就出问题了

    既然把默认python改成了python3的版本,那么这时候yum就出问题了,因为yum貌似不支持python3,开发了这个命令的老哥也不打算继续写支持python3的版本了,所以,如果和python ...

  6. Qt信号槽的一些事 Qt::带返回值的信号发射方式

    一般来说,我们发出信号使用emit这个关键字来操作,但是会发现,emit并不算一个调用,所以它没有返回值.那么如果我们发出这个信号想获取一个返回值怎么办呢? 两个办法:1.通过出参形式返回,引用或者指 ...

  7. 使用 C++11 编写类似 QT 的信号槽——上篇

    了解 QT 的应该知道,QT 有一个信号槽 Singla-Slot 这样的东西.信号槽是 QT 的核心机制,用来替代函数指针,将不相关的对象绑定在一起,实现对象间的通信. 考虑为 Simple2D 添 ...

  8. QT窗体间传值总结之Signal&Slot

    在写程序时,难免会碰到多窗体之间进行传值的问题.依照自己的理解,我把多窗体传值的可以使用的方法归纳如下: 1.使用QT中的Signal&Slot机制进行传值: 2.使用全局变量: 3.使用pu ...

  9. QT 中 关键字讲解(emit,signal,slot)

    Qt中的类库有接近一半是从基类QObject上继承下来,信号与反应槽(signals/slot)机制就是用来在QObject类或其子类间通讯的方法.作为一种通用的处理机制,信号与反应槽非常灵活,可以携 ...

  10. Python2+python3——多版本启动和多版本pip install问题

    背景描述: python2版本都知道维护到2020年,目前使用python的很大一部分用户群体都开始改安装并且使用最新版的python3版本了,python2和python3在编程大的层面不曾改变,有 ...

随机推荐

  1. AtCoder Beginner Contest 357

    ABC357总结 AtCoder Beginner Contest 357 A - Sanitize Hands 翻译 有一瓶消毒剂,正好可以消毒 \(M\) 双手. \(N\) 名外星人陆续前来消毒 ...

  2. mysql报错 a foreign key constraint fails(外键约束错误)

    报错信息如下: (pymysql.err.IntegrityError) (1452, u'Cannot add or update a child row: a foreign key constr ...

  3. JS 中 == 和 === 区别是什么?

    a.对于string,number等基础类型,==和===有区别:不同类型间比较,==之比较"转化成同一类型后的值"看"值"是否相等,===如     果类型不 ...

  4. CodeFormer一款既能图像修复、还能视频增强去码的AI软件(下载介绍)

    CodeFormer是一款强大的人工智能工具,主要用于图像和视频的修复和增强.它基于深度学习技术,特别是人脸复原模型,可以轻松修复和增强面部图像,提升照片和视频的质量和视觉效果 工作原理 1.通过自动 ...

  5. win10系统常用命令(netstat、ping、telnet、sc、netsh命令)

    netstat命令 1. 查找端口占用 netstat -ano netstat -ano | findstr 5000 ping命令 ping 192.168.1.1 ping baidu.com ...

  6. spark共享变量---广播变量和累加变量

    从三个方面来分析:1.什么时候使用,2.原理是什么3.性能上有什么优化 累加变量:--(自定义累加器很重要) 使用场景:累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数,如:统计日志中空行 ...

  7. 光伏储能电厂设备连接iec61850平台解决方案

    在当今日益发展的电力系统中,光伏储能技术以其独特的优势逐渐崭露头角,成为可再生能源领域的重要组成部分.而在光伏储能系统的运行与监控中,通信协议的选择与实现则显得至关重要.本文将重点介绍光伏储能系统中的 ...

  8. P9358 题解

    不难发现,最开始有 \(n\) 条链,并且由于每个点最多有一个桥,所以我们的交换操作实际上等价于将相邻的两条链断开,然后将它们后半部分交换.并且每个点在路径中的相对位置不变. 于是考虑维护这些链. 有 ...

  9. win11 vmware16 启动虚拟机引起蓝屏

    前言 在win11 上安装 vmware16, 之后安装ubuntu16时,一打开ubuntu虚拟机就触发系统蓝屏. 正文 我改了两个地方: 控制面板->程序->启用或关闭Windows功 ...

  10. 对于 vue3.0 特性你有什么了解的吗?

    Vue 3.0 的目标是让 Vue 核心变得更小.更快.更强大,因此 Vue 3.0 增加以下这些新特性: (1)监测机制的改变3.0 将带来基于代理 Proxy 的 observer 实现,提供全语 ...