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

时间:2015-08-04 08:26:52

标签: python thread-safety python-multithreading

我的项目涉及为客户群体处理图像。客户端发送压缩的图像文件,每个图像触发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()即时添加用户和命令,而队列中的命令仍然是正在处理。到目前为止,这在我的初始测试中看起来效果很好,但这在理论上是线程安全吗?如果没有,我需要做些什么才能确保它?

3 个答案:

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