我的项目涉及为客户群体处理图像。客户端发送压缩的图像文件,每个图像触发ImageMagick命令行脚本。我试图解决的问题是,如果这些命令按照我收到的顺序排队,那么需要处理10k图像的客户端将占用所有资源数小时。我的解决方案是循环每个客户端的队列,这样每个人都可以平等地减慢速度。我创建了这个类来实现这个:
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_or_append()
的else子句中看到的那样,cycle_list
是这样的词典列表:
{"user": "test1", "commands": ["command1", "command2"]},
{"user": "test2", "commands": ["x", "y", "z"]},
{"user": "test3", "commands": ["a", "b", "c"]}
(删除了实际命令,使用了示例字符串)
cmd_gen()
的单个实例将用于将命令提供给我的shell,我将使用create_or_append()
即时添加用户和命令,而队列中的命令仍然是正在处理。到目前为止,这在我的初始测试中看起来效果很好,但这在理论上是线程安全吗?如果没有,我需要做些什么才能确保它?
答案 0 :(得分:1)
我对以下部分的线程安全性有疑问:
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
}]
如果2个线程运行方法create_or_append
,那么2个线程可能会在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_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
答案 1 :(得分:1)
你的类绝对不是线程安全的,因为你改变了它的实例属性:
update_status
中,您突变self.processing
值在create_or_append
中修改self.cycle_list
如果没有锁定这些属性,您的类就不会是线程安全的。
旁注:始终在__init__
方法中初始化所有实例属性。由于您在代码中使用self.processing
,因此它应位于__init__
答案 2 :(得分:1)
我以为我会像你描述的那样创建一个通用的平衡队列 - 这就是结果。我认为仍然存在一些病态案例,其中用户可以按顺序处理许多作业,但是它会涉及其他用户添加特定时间/订单的作业,所以我不认为它会在实际工作中发生而不能除非有多个用户勾结,否则将被利用。
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)