【问题标题】:How do you generate random unique identifiers in a multi process and multi thread environment?如何在多进程和多线程环境中生成随机唯一标识符?
【发布时间】:2009-11-06 12:43:23
【问题描述】:

我想出的每一个解决方案都不是线程保存。

def uuid(cls,db):
    u = hexlify(os.urandom(8)).decode('ascii')
    db.execute('SELECT sid FROM sessions WHERE sid=?',(u,))
    if db.fetch(): u=cls.uuid(db)
    else: db.execute('INSERT INTO sessions (sid) VALUES (?)',(u,))
    return u

【问题讨论】:

    标签: python sql mod-wsgi


    【解决方案1】:
    import os, threading, Queue
    
    def idmaker(aqueue):
      while True:
        u = hexlify(os.urandom(8)).decode('ascii')
        aqueue.put(u)
    
    idqueue = Queue.Queue(2)
    
    t = threading.Thread(target=idmaker, args=(idqueue,))
    t.daemon = True
    t.start()
    
    def idgetter():
      return idqueue.get()
    

    队列通常是在 Python 中同步线程的最佳方式——这种情况足够频繁,以至于在设计多线程系统时,您的第一个想法应该是“我怎样才能最好地使用队列来做到这一点”。基本思想是让一个线程完全“拥有”共享资源或子系统,并让所有其他“工作”线程仅通过获取和/或放入该专用线程使用的队列来访问资源(队列本质上是线程安全的) .

    在这里,我们创建了一个长度仅为 2 的 idqueue(我们不希望 id 生成变得疯狂,预先创建很多 id,这会浪费内存并耗尽熵池 - 不确定是否2 是最佳的,但最佳位置肯定是一个非常小的整数;-),所以 id 生成器线程在尝试添加第三个时会阻塞,并等待队列中的一些空间打开。 idgetter(也可以简单地由顶级赋值定义,idgetter = idqueue.get)通常会找到一个已经存在的 id 并等待(并为下一个腾出空间!)——如果没有,它本质上会阻塞并等待,一旦 id 生成器将新的 id 放入队列中就会唤醒。

    【讨论】:

    • 所有 WSGIDaemonProcesses 都可以访问 idqueue.get() 吗?
    • @Gert,如果你有多个进程而不是线程,你可以用 multiprocessing.Queue 而不是 Queue.Queue 来做同样的事情(后者用于线程,而不是多处理)。跨度>
    • Mod wsgi 是一个多处理和多线程的 wsgi 服务器,如何同时使用?
    • multiprocessing.Queue 与线程以及进程一起工作(如果你只有线程,那就太矫枉过正了)。不确定 wsgi 守护进程是如何启动的,即它们可以共享哪些数据结构,或者 IOW 如何最好地安排它们相互通信。
    • @Alex:你在这里解决什么问题?你真的认为os.urandom() 在线程/多进程环境中不安全吗?
    【解决方案2】:

    您的算法没问题(只要您的 DB API 模块是安全的,线程是安全的)并且可能是最好的方法。它永远不会给您重复(假设您在 sid 上有 PRIMARY 或 UNIQUE 键),但您在 INSERT 上获得IntegrityError 异常的机会微乎其微。但是你的代码看起来不太好。最好使用尝试次数有限的循环而不是递归(如果代码中的某些错误可能会变得无限):

    for i in range(MAX_ATTEMPTS):
        sid = os.urandom(8).decode('hex')
        db.execute('SELECT COUNT(*) FROM sessions WHERE sid=?', (sid,))
        if not db.fetchone()[0]:
            # You can catch IntegrityError here and continue, but there are reasons
            # to avoid this.
            db.execute('INSERT INTO sessions (sid) VALUES (?)', (sid,))
            break
    else:
        raise RuntimeError('Failed to generate unique session ID')
    

    您可以增加读取的随机字符数,以减少失败的机会。 base64.urlsafe_b64encode() 是您的朋友,如果您想让 SID 更短,但是您必须确保您的数据库对此列使用区分大小写的比较(MySQL 的 VARCHAR 不适合,除非您为其设置二进制排序规则,但 VARBINARY 可以)。

    【讨论】:

    • 您能解释一下避免捕获 IntegrityError 的原因(代码中的第一个注释)吗? Condition 似乎不太可靠,因为在执行查询时仍有很小的机会获得 IntegrityError。这些是性能原因吗?
    • @Anton 一些数据库不允许IntegrityError之后的后续SQL语句直到事务结束。因此,您必须从事务开始回滚并重复所有步骤。在某些特定情况下(例如当事务中没有其他查询时)并不难,但没有通用的解决方案。在我的解决方案中,竞争条件在大多数项目的生命周期内发生一次的机会微乎其微。
    【解决方案3】:

    我建议对 Denis 接受的答案进行小修改:

    for i in range(MAX_ATTEMPTS):
        sid = os.urandom(8).decode('hex')
        try:
            db.execute('INSERT INTO sessions (sid) VALUES (?)', (sid,))
        except IntegrityError:
            continue
        break
    else:
        raise RuntimeError('Failed to generate unique session ID')
    

    我们只是尝试插入而不明确检查生成的 ID。插入非常很少会失败,所以我们通常只需要进行一次数据库调用,而不是两次。

    这将通过减少数据库调用来提高效率,而不会影响线程安全(因为这将由数据库引擎有效处理)。

    【讨论】:

    • 此方案在不良情况下会导致交易出现问题。
    • 你能解释一下究竟需要发生什么才能让请求失败吗?当这个线程即将发生异常时,另一个线程需要做什么?
    • 我真的看不出这种解决方案在哪种情况下会出现更多问题,但我可能会遗漏一些东西。你能详细说明吗?实际上,我认为这两种解决方案都不完全适合交易。对于事务,我建议让数据库引擎生成 UID。参见例如dev.mysql.com/doc/refman/5.1/en/…
    • 抱歉 sqlite3 无法生成。
    【解决方案4】:

    如果你需要线程安全,为什么不给你随机数生成器一个使用共享锁的函数:

    import threading
    lock = threading.Lock()
    def get_random_number(lock)
        with lock:
            print "This can only be done by one thread at a time"
    

    如果所有调用get_random_number的线程都使用同一个锁实例,那么每次只有一个线程可以创建一个随机数。

    当然,您也刚刚使用此解决方案在您的应用程序中创建了一个瓶颈。根据您的要求,还有其他解决方案,例如创建唯一标识符块,然后并行使用它们。

    【讨论】:

    • 两个独立的 WSGIDaemonProcesses 每个都有自己的锁,仍然可以执行相同的选择吗?
    【解决方案5】:

    我认为不需要调用数据库:

    >>> import uuid
    
    # make a UUID based on the host ID and current time
    >>> uuid.uuid1()
    UUID('a8098c1a-f86e-11da-bd1a-00112444be1e')
    

    来自this page

    【讨论】:

    • 是否足够随机以至于人们无法预测 uuid 的
    • 对于 99% 的应用程序来说应该足够随机,但我认为它在密码学上并不安全
    • UUID 被设计为全局唯一的(这里不需要),但不是不可预测的(虽然这对于会话是强制性的)。
    【解决方案6】:

    我会从线程唯一 ID 开始,然后(以某种方式)将它与线程本地计数器连接起来,然后通过加密哈希算法提供给它。

    【讨论】:

    • +1 用于哈希技术,尽管有模块可以生成 GUID。
    • 多进程呢? (WSGIDaemonProcess processes=2 个线程=5) 不可能创建一个唯一的线程 id?我认为线程锁是唯一的解决方案?
    • 您使用另一个状态(唯一进程 ID)并将其与线程唯一 ID 和线程本地计数器连接起来,然后散列。仅当您处于“不是大量线程和进程”的领域时才有效,因为您可能不想为您的计数器突发 64 位,但这会给您一个线程本地计数器,例如 32位,然后是 16 位用于进程 ID,另外 16 位用于线程 ID,在合理的空间内。
    【解决方案7】:

    如果您绝对需要针对数据库验证 uid 并避免竞争条件,请使用事务:

    BEGIN TRANSACTION
    SELECT COUNT(*) FROM sessions WHERE sid=%s
    INSERT INTO sessions (sid,...) VALUES (%s,...)
    COMMIT
    

    【讨论】:

    • SELECT 不会创建锁,因此这与 select 在开始 trans 之前的工作方式相同。
    • 不。想象一下当两个线程同时为同一个 sid 执行 SELECT 并收到 False 时会发生什么。您将有两个插入一个接一个。在事务中包装单个语句没有任何意义。
    【解决方案8】:

    每个线程中没有唯一的数据吗?我很难想象两个线程具有完全相同的数据。虽然我不排除这种可能性。

    过去,当我做这种性质的事情时,线程通常会有一些独特的东西。用户名或客户名或类似性质的东西。例如,我的解决方案是连接用户名和当前时间(以毫秒为单位),然后对该字符串进行哈希处理并获得哈希的十六进制摘要。这给了一个很好的字符串,它总是相同的长度。

    两个线程中的两个不同的 John Smith(或其他)在同一毫秒内生成 id 的可能性非常小。如果这种可能性让人感到紧张,那么可能需要上述锁定路线。

    如前所述,已经有获取 GUID 的例程。我个人喜欢摆弄哈希函数,所以我按照在大型多线程系统上提到的方式成功地推出了自己的方法。

    最终由您来决定您是否真的有包含重复数据的线程。一定要选择一个好的散列算法。我已经成功地使用了 md5,但我读过它可能会与 md5 产生哈希冲突,尽管我从未这样做过。最近一直在用sha1。

    【讨论】:

      【解决方案9】:

      mkdtemp 应该是线程安全的、简单且安全的:

      def uuid():
          import tempfile,os
          _tdir = tempfile.mkdtemp(prefix='uuid_')
          _uuid = os.path.basename(_tdir)
          os.rmdir(_tdir)
          return _uuid
      

      【讨论】:

      • 对于会话 id 的磁盘使用量不会太多吗?
      • 否,返回前查看 os.rmdir。必须有人来做这项工作。在我的例子中,这只是内核操作系统。任何其他实现,如 DB 等,都会在此之上添加层。无论如何,我正在考虑需要目录的会话管理。
      • 删除目录可能会导致冲突?当然这需要调查并且可能与 OS/glibc 相关。当不再需要 uuid 时,您可以离开目录并删除,例如 session logout/timeout 。必须有人保存数据。在我的示例中,它只是操作系统文件系统。正如你所看到的,我一直在考虑参加会议,我的错 ;-)
      猜你喜欢
      • 1970-01-01
      • 2021-10-13
      • 2023-04-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-16
      相关资源
      最近更新 更多