如何对我的 Python 锁进行排队?

问题描述 投票:0回答:3

有没有办法让Python锁排队?到目前为止,我在代码中一直假设 threading.lock 在队列上操作。看起来它只是把锁给了一个随机的储物柜。这对我来说很糟糕,因为我正在工作的程序(游戏)高度依赖于以正确的顺序获取消息。 python中有队列锁吗?如果是这样,我会损失多少处理时间?

python multithreading synchronization locking
3个回答
8
投票

我完全同意那些声称你可能以一种徒劳的方式思考这个问题的评论。 锁提供序列化,并且“根本不”旨在提供排序。 执行订单的标准、简单且可靠的方法是使用 Queue.Queue

CPython 将其留给操作系统来决定获取锁的顺序。  在大多数系统上,这看起来或多或少是“随机的”。  这是无法改变的。

也就是说,我将展示一种实现“先进先出锁”的方法。 它既不难也不容易 - 介于两者之间 - 你不应该使用它;-)恐怕只有你可以回答你的“我会在处理时间上损失多少?”问题 - 我们不知道您使用锁的频率,或者您的应用程序引发了多少锁争用。 不过,通过研究这段代码,您可以得到一个粗略的感觉。

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

这是一个小测试驱动程序,可以通过明显的方式更改它以使用此
QLock

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))

刚刚在我的盒子上,使用 
threading.Lock

运行 3 次产生了以下输出:

BACDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSUVWXYZT
ABCEDFGHIJKLMNOPQRSTUVWXYZ

所以这当然不是随机的,但也不是完全可预测的。  使用 
QLock

运行它,输出应该始终是:

ABCDEFGHIJKLMNOPQRSTUVWXYZ



3
投票

我担心的是,如果锁没有按 FIFO 顺序释放,很可能会发生线程饥饿,这对我的软件来说将是可怕的。

读完之后,我消除了恐惧,意识到每个人都在说:如果你想要这个,那你就做错了。另外,我确信您可以依靠操作系统来完成其工作,而不会让您的线程挨饿。

为了达到这一点,我做了一些挖掘以更好地了解锁在 Linux 下的工作原理。我首先查看了 pthreads (Posix Threads) 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

它解释了一些关于解锁策略、futex 的工作原理以及在 Linux 内核中的实现方式,等等。

在那里我找到了我想要的东西。它解释了 futexes 在内核中的实现方式,使得解锁主要以 FIFO 顺序完成(以增加公平性)。但是,这并不能保证,并且一个线程可能会暂时跳线。他们允许这样做不会使代码过于复杂,并允许他们实现良好的性能,而不会因为强制执行 FIFO 顺序的极端措施而失去它。

所以基本上,你所拥有的是:

POSIX 标准对互斥锁的锁定和解锁顺序没有任何要求。任何实现都可以自由地按照自己的意愿进行,因此如果您依赖此顺序,您的代码将无法移植(即使在同一平台的不同版本之间也是如此)。

pthreads 库的 Linux 实现依赖于称为 futex 的功能/技术来实现互斥体,并且它主要尝试对互斥体进行 FIFO 样式解锁,但不能保证会按该顺序完成。


1
投票
FIFO

队列: FIFO = [5,79,3,2,78,1,9...]

您将尝试获取锁,如果不能,则将尝试线程的 ID (
FIFO.insert(0,threadID)

) 推到队列的前面,每次释放锁时,请确保是否有线程想要获取锁锁它的线程 ID 必须位于队列末尾 (

threadID == FIFO[-1]
)。如果线程确实拥有队列末尾的线程 ID,则让它获取锁,然后将其弹出 (
FIFO.pop()
)。根据需要重复。
    

© www.soinside.com 2019 - 2024. All rights reserved.