Python多线程通信效率
我不熟悉python多任务处理。我用的是老式的方式: 我从threading.Thread继承,并使用queue.queue队列向主线程发送消息或从主线程发送消息 这是我的基本线程类:Python多线程通信效率,python,multithreading,performance,queue,python-multithreading,Python,Multithreading,Performance,Queue,Python Multithreading,我不熟悉python多任务处理。我用的是老式的方式: 我从threading.Thread继承,并使用queue.queue队列向主线程发送消息或从主线程发送消息 这是我的基本线程类: class WorkerGenerico(threading.Thread): def __init__(self, task_id, input_q=None, output_q=None, keep_alive=300): super(WorkerGenerico, self).__i
class WorkerGenerico(threading.Thread):
def __init__(self, task_id, input_q=None, output_q=None, keep_alive=300):
super(WorkerGenerico, self).__init__()
self._task_id = task_id
if input_q is None:
self._input_q = queue.Queue()
else:
if isinstance(input_q, queue.Queue):
self._input_q = input_q
else:
raise TypeError("input_q debe ser del tipo queue.Queue")
if output_q is None:
self._output_q = queue.Queue()
else:
if isinstance(output_q, queue.Queue):
self._output_q = output_q
else:
raise TypeError("input_q debe ser del tipo queue.Queue")
if not isinstance(keep_alive, int):
raise TypeError("El valor de keep_alive debe der un int.")
self._keep_alive = keep_alive
self.stoprequest = threading.Event()
# def run(self):
# Implement a loop in subclases which checks if self.has_orden_parada() is true in order to stop.
def join(self, timeout=None):
self.stoprequest.set()
super(WorkerGenerico, self).join(timeout)
def gracefull_stop(self):
self.stoprequest.set()
def has_orden_parada(self):
return self.stoprequest.is_set()
def put(self,texto, block=True, timeout=None):
return self._input_q.put(texto, block=block, timeout=timeout)
def get(self, block=True, timeout=None):
return self._output_q.get(block=block, timeout=timeout)
我的问题是,与在主线程中存储queue并使用queue.get()相比,从外部调用WorkerGenerico.get()的代价有多大这两种方法在性能上看起来相似,都有小的非频繁控制消息,但是,我想非常频繁的调用会使方法B值得使用:
我猜模式A更消耗资源(它必须以某种方式从外部线程调用该方法并将队列定义传递回,我猜损失取决于Python实现),但是,最终代码更具可读性和直观性
如果我必须根据其他语言的经验来判断,我会说方法B更好,是吗?
方法A:
def main()
worker = WorkerGenerico(task_id=1)
worker.start()
print(worker.get())
方法B:
def main()
input_q = Queue()
output_q = Queue()
worker = WorkerGenerico(task_id=1, input_q=input_q, output_q=output_q)
worker.start()
print(output_q.get())
顺便说一句:为了完整性,我想分享一下我现在的做法。这两种方法的混合为线程提供了一个很好的信封:
class EnvoltorioWorker:
def __init__(self, task_id, input_q=None, output_q=None, keep_alive=300):
if input_q is None:
self._input_q = queue.Queue()
else:
if isinstance(input_q, queue.Queue):
self._input_q = input_q
else:
raise TypeError("input_q debe ser del tipo queue.Queue")
if output_q is None:
self._output_q = queue.Queue()
else:
if isinstance(output_q, queue.Queue):
self._output_q = output_q
else:
raise TypeError("input_q debe ser del tipo queue.Queue")
self.worker = WorkerGenerico(task_id, input_q, output_q, keep_alive)
def put(self, elem, block=True, timeout=None):
return self._input_q.put(elem, block=block, timeout=timeout)
def get(self, block=True, timeout=None):
return self._output_q.get(block=block, timeout=timeout)
我使用EnvoltorioWorker.worker.*调用联接或其他外部控制方法,并使用EnvoltorioWorker.get/EnvoltorioWorker.put与内部类正确通信,如下所示:
def main()
worker_container = EnvoltorioWorker(task_id=1)
worker_container.worker.start()
print(worker_container.get())
通常,如果不需要对worker进行其他访问,我也会在EnvoltorioWorker中为start()、join()和nonwait_stop()创建接口
它可能看起来很虚假,可能有更好的方法来实现这一点,因此:
哪种方法(A或B)更适合练习?在Python中,从线程继承是处理线程的正确方法吗?我在分布式环境中使用dispycos和类似的信封与线程通信
编辑:刚刚注意到我忘了翻译类中的注释和一些字符串,但它们足够简单,所以我认为它是可读的。我有时间时会编辑它
有什么想法吗?您的队列并没有真正存储在线程中。假设这里是CPython,所有对象都存储在堆上,线程只有一个私有堆栈。堆上的对象在同一进程中的所有线程之间共享
Python中的内存管理涉及包含所有Python对象和数据结构的私有堆。这个私有堆的管理由Python内存管理器在内部确保。Python内存管理器有不同的组件,它们处理各种动态存储管理方面的问题,如共享、分段、预分配或缓存
由此可知,这不是对象(队列)位于何处的问题,因为它总是在堆上。Python中的变量(名称)只是对这些对象的引用
这里影响运行时的是通过嵌套函数/方法调用向堆栈中添加多少调用帧,以及需要多少字节码指令。那么这对时间安排有什么影响呢
基准 考虑以下队列和工作进程的虚拟设置。为了简单起见,这里没有对虚拟工作者进行线程化,因为在我们假装只排空预填充队列的场景中,线程化不会影响计时
class Queue:
def get(self):
return 1
class Worker:
def __init__(self, queue):
self.queue = queue
self.quick_get = self.queue.get # a reference to a method as instance attribute
def get(self):
return self.queue.get()
def quick_get_method(self):
return self.quick_get()
您可以看到,Worker
有两个版本的get方法,get
以您定义它的方式,以及quick\u get\u method
,这是一个字节码指令,比我们稍后将看到的更短。worker实例不仅包含对队列
实例的引用,而且还通过self.quick\u get
直接指向队列.get
,这是我们节省一条指令的地方
现在,在IPython会话中,从伪队列对所有可能的.get()
进行基准测试的计时:
q = Queue()
w = Worker(q)
%timeit q.get()
285 ns ± 1.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit w.get()
609 ns ± 2.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit w.quick_get()
286 ns ± 0.756 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit w.quick_get_method()
555 ns ± 0.855 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
请注意,q.get()
和w.quick\u get()
之间的计时没有区别。
还要注意的是,与传统的w.get()
相比,w.quick\u get\u method()
的时间安排有所改进。使用Worker方法
在队列上调用get()
,与q.get()
和w.quick\u get()
相比,仍然几乎将计时增加了一倍。为什么呢
通过使用dis
模块,可以获得解释器正在处理的Python字节码指令的可读版本
import dis
dis.dis(q.get)
3 0 LOAD_CONST 1 (1)
2 RETURN_VALUE
dis.dis(w.get)
8 0 LOAD_FAST 0 (self)
2 LOAD_ATTR 0 (queue)
4 LOAD_METHOD 1 (get)
6 CALL_METHOD 0
8 RETURN_VALUE
dis.dis(w.quick_get)
3 0 LOAD_CONST 1 (1)
2 RETURN_VALUE
dis.dis(w.quick_get_method)
11 0 LOAD_FAST 0 (self)
2 LOAD_METHOD 0 (quick_get)
4 CALL_METHOD 0
6 RETURN_VALUE
记住我们的虚拟队列。get
这里只返回1。您可以看到q.get
与w.quick\u get
一样,这也反映在我们之前看到的计时中。请注意,w.quick_get_method
直接加载quick_get
,这只是对象队列的另一个名称/变量。get
正在引用
您还可以借助dis
模块获得打印的堆栈深度:
def print_stack_depth(f):
print(*[s for s in dis.code_info(f).split('\n') if
s.startswith('Stack size:')]
)
print_stack_depth(q.get)
Stack size: 1
print_stack_depth(w.get)
Stack size: 2
print_stack_depth(w.quick_get)
Stack size: 1
print_stack_depth(w.quick_get_method)
Stack size: 2
不同方法之间的字节码和计时差异意味着(不那么令人惊讶),添加另一帧(通过添加另一种方法)会对性能造成最大的影响
回顾 上面的分析并不是不使用额外的辅助方法来调用引用对象(queue.get)上的方法的隐式辩护。为了可读性、日志记录和更简单的调试,这样做是正确的。例如,您还可以在Stdlib的
多处理.pool.pool
中找到诸如Worker.quick\u get\u method
之类的优化,它在内部也使用队列
从基准测试的角度来看,几百纳秒并不多(对于Python)。在Python3中,线程可以容纳字节码的默认最大时间间隔为5毫秒,因此每次执行字节码的时间间隔为5毫秒。这是5*1000*1000纳秒
与多线程引入的开销相比,几百纳秒也很小。例如,我发现在一个线程中的queue.put(integer)
之后添加20μs睡眠