Python Threading.Event半忙等待的更好解决方案

7
我将使用标准的Threading.Event进行翻译: 主线程进入一个循环,其中运行以下代码:
event.wait(60)

其他块在等待回复可用之前会阻塞请求,然后启动一个:

event.set()

我本来期望主线程会选择40秒,但实际情况并非如此。 这是从Python 2.7源码Lib/threading.py中得到的:

# Balancing act:  We can't afford a pure busy loop, so we
# have to sleep; but if we sleep the whole timeout time,
# we'll be unresponsive.  The scheme here sleeps very
# little at first, longer as time goes on, but never longer
# than 20 times per second (or the timeout time remaining).
endtime = _time() + timeout
delay = 0.0005 # 500 us -> initial delay of 1 ms
while True:
   gotit = waiter.acquire(0)
   if gotit:
       break
   remaining = endtime - _time()
   if remaining <= 0:
       break
   delay = min(delay * 2, remaining, .05)
   _sleep(delay)

我们得到的是每500微秒运行一次select系统调用。这会对机器造成明显的负载,因为它有一个非常紧密的select循环。
请问是否有人能够解释一下其中的平衡问题,并说明为什么它与线程等待文件描述符不同。
其次,是否有更好的方法来实现大部分时间处于睡眠状态的主线程而不需要如此紧密的循环?
2个回答

3

最近我也遇到了同样的问题,而且我也将其追踪到threading模块中的这个代码块。

真是够糟糕的。

解决方法有两种:要么重载threading模块,要么迁移到已经修复了这个实现问题的python3版本。

在我的情况下,迁移到python3会耗费大量精力,所以我选择了前者。我的做法是:

  1. 我使用cython创建了一个快速的.so文件,其中包含调用相应的pthread_mutex_*函数的Python函数,并链接到libpthread。特别是,我们感兴趣的任务最相关的函数是pthread_mutex_timedlock
  2. 我创建了一个新的threading2模块,并将所有代码库中的import threading行替换为import threading2。在threading2中,我重新定义了所有来自threading的相关类(LockConditionEvent),以及我经常使用的来自Queue的类(QueuePriorityQueue)。Lock类完全是使用pthread_mutex_*函数重新实现的,但其余部分要容易得多——我只需子类化原始类(例如threading.Event),并重写__init__以创建我的新Lock类型。剩下的都很好用。

Lock类型的实现与threading中的原始实现非常相似,但我将acquire的新实现基于我在python3threading模块中找到的代码(自然而然地,这比上述“平衡行为”块简单得多)。这部分相当容易。

(顺便说一下,在我的情况下,结果是我的大规模多线程进程加速了30%。比我预期的还要多。)


2

我完全同意你的看法,这很糟糕。

目前,我使用一个简单的select调用,没有超时,并在创建管道后监听管道。 通过在管道中写入字符来唤醒。

请参见gunicorn的sleepwakeup函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接