有没有办法让 python 锁排队?到目前为止,我一直在我的代码中假设 threading.lock 在队列上运行。看起来它只是把锁给了一个随机的储物柜。这对我不利,因为我正在工作的程序(游戏)高度依赖于以正确的顺序获取消息。python中有排队锁吗?如果是这样,我会在处理时间上损失多少?
3 回答
我完全同意那些声称你可能正在以一种徒劳的方式思考这个问题的评论。锁提供序列化,并且根本不打算提供排序。执行订单的标准、简单且可靠的方式是使用Queue.Queue
CPython 让操作系统决定获取锁的顺序。在大多数系统上,这似乎或多或少是“随机的”。这是无法改变的。
也就是说,我将展示一种实现“FIFO 锁”的方法。这既不难也不容易 - 介于两者之间 - 你不应该使用它;-)恐怕只有你能回答你的“我会在处理时间上损失多少?” 问题 - 我们不知道您使用锁的频率如何,或者您的应用程序引发了多少锁争用。不过,您可以通过研究这段代码来获得粗略的感觉。
import threading, collections
class QLock:
def __init__(self):
self.lock = threading.Lock()
self.waiters = collections.deque()
self.count = 0
def acquire(self):
self.lock.acquire()
if self.count:
new_lock = threading.Lock()
new_lock.acquire()
self.waiters.append(new_lock)
self.lock.release()
new_lock.acquire()
self.lock.acquire()
self.count += 1
self.lock.release()
def release(self):
with self.lock:
if not self.count:
raise ValueError("lock not acquired")
self.count -= 1
if self.waiters:
self.waiters.popleft().release()
def locked(self):
return self.count > 0
这是一个小测试驱动程序,可以通过明显的方式更改它以使用 thisQLock
或 a threading.Lock
:
def work(name):
qlock.acquire()
acqorder.append(name)
from time import sleep
if 0:
qlock = threading.Lock()
else:
qlock = QLock()
qlock.acquire()
acqorder = []
ts = []
for name in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
t = threading.Thread(target=work, args=(name,))
t.start()
ts.append(t)
sleep(0.1) # probably enough time for .acquire() to run
for t in ts:
while not qlock.locked():
sleep(0) # yield time slice
qlock.release()
for t in ts:
t.join()
assert qlock.locked()
qlock.release()
assert not qlock.locked()
print "".join(acqorder)
刚才在我的盒子上,使用 3 次运行threading.Lock
产生了这个输出:
BACDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSUVWXYZT
ABCEDFGHIJKLMNOPQRSTUVWXYZ
所以它当然不是随机的,但也不是完全可以预测的。相反,运行它QLock
,输出应始终为:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
我偶然发现了这篇文章,因为我有类似的要求。或者至少我是这么认为的。
我担心的是,如果锁没有按 FIFO 顺序释放,很可能会发生线程饥饿,这对我的软件来说会很糟糕。
读了一点之后,我消除了恐惧,意识到每个人都在说:如果你想要这个,你就做错了。此外,我确信您可以依靠操作系统来完成它的工作,而不会让您的线程饿死。
为了达到这一点,我做了一些挖掘工作以更好地了解锁在 Linux 下的工作原理。我首先查看了 pthreads(Posix 线程)glibc 源代码和规范,因为我在 Linux 上使用 C++ 工作。我不知道 Python 是否在后台使用 pthreads,但我假设它可能是。
在周围的多个 pthreads 引用中,我没有找到任何与解锁顺序有关的规范。
我发现:Linux 上 pthread 中的锁是使用称为futex的内核功能实现的。
http://man7.org/linux/man-pages/man2/futex.2.html
http://man7.org/linux/man-pages/man7/futex.7.html
这些页面中第一页的参考链接将引导您访问此 PDF:
https://www.kernel.org/doc/ols/2002/ols2002-pages-479-495.pdf
它解释了一些关于解锁策略的内容,以及 futexes 如何在 Linux 内核中工作和实现,等等。
在那里我找到了我想要的。它解释了 futexes 在内核中的实现方式使得解锁大多以 FIFO 顺序完成(以增加公平性)。但是,这并不能保证,并且一个线程可能会在一段时间内跳出第一行。他们允许这不会使代码过于复杂,并允许他们获得良好的性能而不会因为强制执行 FIFO 顺序的极端措施而失去它。
所以基本上,你所拥有的是:
POSIX 标准对互斥锁的锁定和解锁顺序没有任何要求。任何实现都可以随心所欲地进行,因此如果您依赖此顺序,您的代码将无法移植(即使在同一平台的不同版本之间也不行)。
pthreads 库的 Linux 实现依赖于一种称为 futex 的特性/技术来实现互斥锁,它主要尝试对互斥锁进行 FIFO 样式的解锁,但不能保证它会按该顺序完成。
是的,您可以使用线程 ID 列表创建一个FIFO队列:
FIFO = [5,79,3,2,78,1,9...]
您将尝试获取锁,如果不能,则将尝试线程的 ID ( FIFO.insert(0,threadID)
) 推送到队列的前面,并且每次释放锁时,请确保如果线程想要获取锁,则它必须具有队列末尾的线程 ID ( threadID == FIFO[-1]
)。如果线程确实在队列末尾有线程 ID,则让它获取锁,然后将其弹出(FIFO.pop()
)。根据需要重复。