Python 创建线程安全队列平衡器
我的项目涉及为客户处理大量图像。客户端发送压缩后的图像文件,这会为每个图像触发ImageMagick命令行脚本。我试图解决的问题是,如果这些命令按照我接收它们的顺序排队,那么需要处理10k图像的客户端将占用所有资源数小时。我的解决方案是循环使用每个客户机的队列,这样每个人都能平等地降低彼此的速度。我创建这个类是为了实现这个:Python 创建线程安全队列平衡器,python,thread-safety,python-multithreading,Python,Thread Safety,Python Multithreading,我的项目涉及为客户处理大量图像。客户端发送压缩后的图像文件,这会为每个图像触发ImageMagick命令行脚本。我试图解决的问题是,如果这些命令按照我接收它们的顺序排队,那么需要处理10k图像的客户端将占用所有资源数小时。我的解决方案是循环使用每个客户机的队列,这样每个人都能平等地降低彼此的速度。我创建这个类是为了实现这个: class QueueBalancer(): def __init__(self, cycle_list=[]): self.cycle_list
class QueueBalancer():
def __init__(self, cycle_list=[]):
self.cycle_list = cycle_list
self.update_status()
def cmd_gen(self):
index = -1
while True:
try:
if self.cycle_list:
self.processing = True
index += 1
commands = self.cycle_list[index]["commands"]
if commands:
command = commands.pop(0)
if len(commands) == 0:
del self.cycle_list[index]
index -= 1
self.update_status()
yield command
else:
yield None
except IndexError:
index = -1
def get_user(self, user):
return next((item for item in self.cycle_list[0] if item["user"] == user), None)
def create_or_append(self, user, commands):
existing_user = self.get_user(user)
if existing_user:
index = self.cycle_list.index(existing_user)
self.cycle_list[index]["commands"] += commands
else:
self.cycle_list += [{
"user" : user,
"commands" : commands
}]
def update_status(self):
if next((item for item in self.cycle_list if item["commands"] != []), None):
self.processing = True
else:
self.processing = False
def status(self):
return self.processing
从create\u或\u append()
的else子句中可以看到,cycle\u列表是这样一个字典列表:
{"user": "test1", "commands": ["command1", "command2"]},
{"user": "test2", "commands": ["x", "y", "z"]},
{"user": "test3", "commands": ["a", "b", "c"]}
(删除了实数命令,使用了示例字符串)
将使用cmd\u gen()
的单个实例将命令馈送到shell中,我将使用create\u或\u append()
动态添加用户和命令,而队列中的命令仍在处理中。到目前为止,在我的初始测试中,这似乎非常有效,但这在理论上是线程安全的吗?如果没有,我需要做什么来确保它是正确的?我对以下部件的螺纹安全性有疑问:
def create_or_append(self, user, commands):
existing_user = self.get_user(user)
if existing_user:
index = self.cycle_list.index(existing_user)
self.cycle_list[index]["commands"] += commands
else:
self.cycle_list += [{
"user" : user,
"commands" : commands
}]
如果有两个线程运行方法create\u或\u append
,则这两个线程可能位于else闭包中,然后会损坏一点您的数据。也许在这个函数中定义一个锁是个好主意
from threading import Lock
class QueueBalancer():
def __init__(self, cycle_list=None):
self.cycle_list = [] if cycle_list is None else cycle_list
self.lock = Lock()
# .../...
def create_or_append(self, user, commands):
with self.lock:
# ...
编辑:正如@matino所说的,您还可能对update\u status
功能有一些问题,因为它修改了processing
实例属性。我建议对它使用另一个锁,以确保它是线程安全的
def update_status(self):
with self.update_lock:
if next((item for item in self.cycle_list if item["commands"] != []), None):
self.processing = True
else:
self.processing = False
您的类肯定不是线程安全的,因为您改变了它的实例属性:
- 在
update\u status
中,您可以变异self.processing
值
- 在
create\u或\u append
中修改self.cycle\u列表
如果没有这些属性上的锁,您的类将不会是线程安全的
def update_status(self):
with self.update_lock:
if next((item for item in self.cycle_list if item["commands"] != []), None):
self.processing = True
else:
self.processing = False
旁注:始终在方法中初始化所有实例属性。由于您在代码中使用了self.processing
,因此它应该位于\uuu init\uuu
中,我想我可以尝试创建一个像您所描述的通用平衡队列-结果如下。我认为仍然存在一些病态情况,一个用户可以按顺序处理多个作业,但这会涉及其他用户的作业被添加特定的时间/顺序,因此我认为这不会发生在实际工作中,除非多个用户串通,否则无法被利用
from threading import Lock
class UserBalancedJobQueue(object):
def __init__(self):
self._user_jobs = {}
self._user_list = []
self._user_index = 0
self._lock = Lock()
def pop_user_job(self):
with self._lock:
if not self._user_jobs:
raise ValueError("No jobs to run")
if self._user_index >= len(self._user_list):
self._user_index = 0
user = self._user_list[self._user_index]
jobs = self._user_jobs[user]
job = jobs.pop(0)
if not jobs:
self._delete_current_user()
self._user_index += 1
return user, job
def _delete_current_user(self):
user = self._user_list.pop(self._user_index)
del self._user_jobs[user]
def add_user_job(self, user, job):
with self._lock:
if user not in self._user_jobs:
self._user_list.append(user)
self._user_jobs[user] = []
self._user_jobs[user].append(job)
if __name__ == "__main__":
q = UserBalancedJobQueue()
q.add_user_job("tom", "job1")
q.add_user_job("tom", "job2")
q.add_user_job("tom", "job3")
q.add_user_job("fred", "job4")
q.add_user_job("fred", "job5")
for i in xrange(3):
print q.pop_user_job()
print "Adding more jobs"
q.add_user_job("dave", "job6")
q.add_user_job("dave", "job7")
q.add_user_job("dave", "job8")
q.add_user_job("dave", "job9")
try:
while True:
print q.pop_user_job()
except ValueError:
pass
仔细考虑一下,另一种实现方法是记住每个用户上次作业的运行时间,然后根据谁的上次作业最早选择下一个用户。它可能更“正确”,但它会有(可能可以忽略不计)额外的内存开销,为每个用户记住上一次作业时间
编辑:所以这是一个缓慢的一天-这里是另一种方法。我想我更喜欢它,虽然它的速度较慢,因为O(N)搜索用户与最早的前一份工作
from collections import defaultdict
from threading import Lock
import time
class UserBalancedJobQueue(object):
def __init__(self):
self._user_jobs = defaultdict(list)
self._user_last_run = defaultdict(lambda: 0.0)
self._lock = Lock()
def pop_user_job(self):
with self._lock:
if not self._user_jobs:
raise ValueError("No jobs to run")
user = min(
self._user_jobs.keys(),
key=lambda u: self._user_last_run[u]
)
self._user_last_run[user] = time.time()
jobs = self._user_jobs[user]
job = jobs.pop(0)
if not jobs:
del self._user_jobs[user]
return user, job
def add_user_job(self, user, job):
with self._lock:
self._user_jobs[user].append(job)
旁注-你不应该使用可变的默认参数,因为你可能会得到一些意想不到的副作用:def\uuu init\uuuu(self,cycle\u list=[]):
-ref啊,我知道我写它的时候感觉很奇怪。感谢tipThis将cycle\u list
转换为将用户映射到命令列表的dict,可以简化此操作。有“用户”和“命令”键的dicts没有任何作用。考虑到这一点,我认为全面机器人化用户作业可能会有点痛苦,要做到“正确”,我认为你需要做大量的簿记,以了解用户何时被添加/从列表中删除。根据您的用例,每次随机选择一个用户并处理他们的第一个作业可能是可行的(并且会更简单)?我在您的代码中没有看到任何线程,因此很难判断它是否是线程安全的。