Python 创建线程安全队列平衡器

Python 创建线程安全队列平衡器,python,thread-safety,python-multithreading,Python,Thread Safety,Python Multithreading,我的项目涉及为客户处理大量图像。客户端发送压缩后的图像文件,这会为每个图像触发ImageMagick命令行脚本。我试图解决的问题是,如果这些命令按照我接收它们的顺序排队,那么需要处理10k图像的客户端将占用所有资源数小时。我的解决方案是循环使用每个客户机的队列,这样每个人都能平等地降低彼此的速度。我创建这个类是为了实现这个: class QueueBalancer(): def __init__(self, cycle_list=[]): self.cycle_list

我的项目涉及为客户处理大量图像。客户端发送压缩后的图像文件,这会为每个图像触发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\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没有任何作用。考虑到这一点,我认为全面机器人化用户作业可能会有点痛苦,要做到“正确”,我认为你需要做大量的簿记,以了解用户何时被添加/从列表中删除。根据您的用例,每次随机选择一个用户并处理他们的第一个作业可能是可行的(并且会更简单)?我在您的代码中没有看到任何线程,因此很难判断它是否是线程安全的。