Python 在芹菜任务中使用多进程并发机制

Python 在芹菜任务中使用多进程并发机制,python,multiprocessing,celery,Python,Multiprocessing,Celery,我正在尝试与一个只能接受单个TCP连接(内存限制)的设备进行接口,因此仅为每个工作线程启动一个连接并不是一个选项,因为在正常的客户机-服务器情况下,例如数据库连接 我曾尝试使用多处理管理器dict,该dict在线程之间可全局访问,格式如下: clients{(地址,端口):(connection_obj,multiprocessing.Manager.RLock)} 像这样的任务: from celery import shared_task from .celery import manage

我正在尝试与一个只能接受单个TCP连接(内存限制)的设备进行接口,因此仅为每个工作线程启动一个连接并不是一个选项,因为在正常的客户机-服务器情况下,例如数据库连接

我曾尝试使用多处理管理器dict,该dict在线程之间可全局访问,格式如下:

clients{(地址,端口):(connection_obj,multiprocessing.Manager.RLock)}

像这样的任务:

from celery import shared_task
from .celery import manager, clients

@shared_task
def send_command(controller, commandname, args):
    """Send a command to the controller."""
    # Create client connection if one does not exist.
    conn = None
    addr, port = controller
    if controller not in clients:
        conn = Client(addr, port)
        conn.connect()
        lock = manager.RLock()
        clients[controller] = (conn, lock,)
        print("New controller connection to %s:%s" % (addr, port,))
    else:
        conn, lock = clients[controller]

    try:
        f = getattr(conn, commandname) # See if connection.commandname() exists.
    except Exception:
        raise Exception("command: %s not known." % (commandname))

    with lock:
        res = f(*args)
        return res
但是,任务将失败,并出现序列化错误,例如:

\u pickle.PicklingError:无法pickle:线程上的属性查找锁定失败

即使我没有用不可序列化的值调用任务,并且任务也没有尝试返回不可序列化的值,芹菜似乎痴迷于尝试序列化这个全局对象

我错过了什么?您将如何使芹菜任务中使用的客户端设备连接线程安全并在线程之间可访问?示例代码?

。。。
 ...
self._send_bytes(ForkingPickler.dumps(obj))
 File "/usr/lib64/python3.4/multiprocessing/reduction.py", line 50, in dumps
cls(buf, protocol).dump(obj)
_pickle.PicklingError: Can't pickle <class '_thread.lock'>: attribute lookup lock on _thread failed
self.\u发送字节(ForkingPickler.dumps(obj)) 文件“/usr/lib64/python3.4/multiprocessing/reduce.py”,第50行,转储 cls(buf,协议).dump(obj) _pickle.PicklingError:无法pickle:线程上的属性查找锁定失败
在浏览了互联网之后,我意识到我可能在回溯中遗漏了一些重要的东西。在仔细查看回溯后,我意识到不是芹菜试图腌制连接对象,而是多处理。还原。还原用于一边序列化,另一边重新生成


我有一些替代方法来解决这个问题,但是没有一种真正做到我最初想要的,那就是借用客户机库连接对象并使用它,这在多处理和预处理中是不可能的。

使用Redis实现分布式锁管理器怎么样?Redis python客户端具有内置的锁定功能。另外,请参见redis.io上的。即使您使用的是RabbitMQ或其他代理,Redis也是非常轻量级的

例如,作为一名装饰师:

from functools import wraps

def device_lock(block=True):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            return_value = None
            have_lock = False
            lock = redisconn.lock('locks.device', timeout=2, sleep=0.01)
            try:
                have_lock = lock.acquire(blocking=block)
                if have_lock:
                    return_value = func(*args, **kwargs)
            finally:
                if have_lock:
                    lock.release()
            return return_value
        return wrapper
    return decorator

@shared_task
@device_lock
def send_command(controller, commandname, args):
    """Send a command to the controller."""
    ...
您还可以从芹菜任务食谱中使用:

from celery import task
from celery.utils.log import get_task_logger
from django.core.cache import cache
from hashlib import md5
from djangofeeds.models import Feed

logger = get_task_logger(__name__)

LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes

@task(bind=True)
def import_feed(self, feed_url):
    # The cache key consists of the task name and the MD5 digest
    # of the feed URL.
    feed_url_hexdigest = md5(feed_url).hexdigest()
    lock_id = '{0}-lock-{1}'.format(self.name, feed_url_hexdigest)

    # cache.add fails if the key already exists
    acquire_lock = lambda: cache.add(lock_id, 'true', LOCK_EXPIRE)
    # memcache delete is very slow, but we have to use it to take
    # advantage of using add() for atomic locking
    release_lock = lambda: cache.delete(lock_id)

    logger.debug('Importing feed: %s', feed_url)
    if acquire_lock():
        try:
            feed = Feed.objects.import_feed(feed_url)
        finally:
            release_lock()
        return feed.url

    logger.debug(
        'Feed %s is already being imported by another worker', feed_url)

您是否尝试过使用gevent或eventlet Cellery worker而不是进程和线程?在本例中,您将能够使用全局变量或threading.local()来共享连接对象。

啊,我想我的回答有点仓促,因为您想传递相同的连接对象。Mutliprocessing和prefork通常不能很好地处理进程之间的连接和i/o。通常需要建立一个接线柱叉。您是否考虑过从prefork切换到eventlet或gevent以实现并发性,然后实现连接池?我知道这些解决方案-但是我没有使用它的原因是因为它没有实现我想要的功能,即在进程之间共享实际的连接对象并使用已经打开的连接。我试图避免每次运行任务时断开和重新连接。如果我用一个线程运行worker并重用它,那么我可以将连接对象保持为全局对象。我正在考虑为这些客户使用一批单一流程工人。否则,如果我每次发送邮件时都选择连接,那么我将使用Redis进行锁定。在其他解决方案中…我不确定这是否适用于您的情况,但我刚刚记得读过关于
多处理.reduce
,它应该允许在进程之间共享套接字连接。客户端没有使用原始套接字,它是一个具有协议的扭曲连接对象。使用原始套接字或从fd重新定位Twisted connection对象并非易事。我最终找到了如何将Twisted协议包装在现有套接字上的方法,但在我的情况下,它不起作用,因为芹菜消费者作为工作者主进程的独立子进程无法访问所需的文件描述符(存储在Redis中),而设置混乱的unix管道来共享FDs也太麻烦了。我的情况的问题是,设备内存有限,不能有多个连接。。。因此,我决定只让一批员工拥有一个消费者和一台设备。不好!我在用eventlet进行锁定。我可以更努力地找出原因,但没有什么激励,因为我尝试做的阻塞IO性质不适合eventlet/gevent的事件循环性质。