Python多线程通信效率

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

我不熟悉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).__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睡眠