在多进程和多线程环境中如何生成随机唯一标识符?

3

我提出的每个解决方案都不是线程安全的。

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
9个回答

5
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时,id生成器线程将被阻塞,并等待队列中有空间。 idgetter(也可以通过顶级赋值简单定义为idgetter = idqueue.get)通常会发现已经有等待的id(并为下一个id腾出空间!) - 如果没有,则它本质上会阻塞并等待,一旦id生成器将新id放入队列中,它就会立即唤醒。

idqueue.get() 是否可以被所有的 WSGIDaemonProcesses 访问? - Gert Cuykens
1
@Gert,如果你有多个进程而不是线程,你可以使用multiprocessing.Queue来完成相同的事情,而不是Queue.Queue(后者用于线程,而不是用于多进程)。 - Alex Martelli
Mod wsgi是一个多进程和多线程的WSGI服务器,我该如何同时使用它们? - Gert Cuykens
1
多进程队列可以与线程以及进程一起使用(如果只有线程,这就有点过度杀伤了)。不确定WSGI守护进程是如何启动的,即它们可以共享哪些数据结构,或者换句话说,它们如何最好地安排彼此之间的通信。 - Alex Martelli
1
@Alex:你在这里解决什么问题?你真的认为在线程/多进程环境中os.urandom()是不安全的吗? - Denis Otkidach
@Gert,抱歉,注释中的代码是自由流动的,因此完全无法阅读。如果您希望您的代码可读性强,您将需要编辑您的问题并以适当的格式将其添加到那里。 - Alex Martelli

3
你的算法很好(只要你的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')

如果你希望使失败的几率更小,可以增加读取随机字符的数量。如果你想让SID更短,那么base64.urlsafe_b64encode()是你的好朋友,但是你必须确保你的数据库对这些列使用区分大小写的比较(MySQL的VARCHAR不适用,除非你为其设置二进制排序规则,但VARBINARY可以)。


请问你能解释一下为什么要避免捕获IntegrityError(代码中的第一个注释)吗?因为条件似乎不太可靠,由于执行查询时仍有很小的机会出现IntegrityError。这是出于性能原因吗? - Anton Strogonoff
@Anton 有些数据库在 IntegrityError 后不允许执行后续的 SQL 语句,直到事务结束。因此,您必须回滚并从事务开始重复所有步骤。在某些特定情况下(例如,在事务中没有其他查询时),这并不难,但没有通用解决方案。在我提供的解决方案中,竞争条件发生的可能性即使是大多数项目的整个生命周期中也是可以忽略不计的。 - Denis Otkidach

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。这种插入很少会失败,因此我们通常只需要进行一次数据库调用,而不是两次。
这将通过减少数据库调用次数来提高效率,而不会影响线程安全性(因为数据库引擎会有效地处理这个问题)。

这个解决方案在糟糕的情况下会导致事务问题。 - Denis Otkidach
请问您能解释一下这个程序需要发生什么才会失败吗?当这个线程即将出现异常时,其他线程需要做些什么? - Gert Cuykens
我真的看不出在哪种情况下这个解决方案会有更多问题,但我可能漏掉了什么。你能详细说明一下吗?实际上,我认为对于事务,两种解决方案都不是完全合适的。对于事务,我建议让数据库引擎生成UID。例如,请参见http://dev.mysql.com/doc/refman/5.1/en/miscellaneous-functions.html#function_uuid - edvald
抱歉,无法生成sqlite3。 - Gert Cuykens

2
如果您需要线程安全,为什么不将您的随机数生成器放在一个使用共享锁的函数中:
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,仍然能够执行相同的select语句吗? - Gert Cuykens

1

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

>>> import uuid

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

来自此页面


它是否足够随机,以至于人们无法预测UUID? - Gert Cuykens
应该对99%的应用程序足够随机,但我认为它不具备密码学安全性。 - Andomar
UUID被设计为全球唯一的(这在此处并非必需),但不是不可预测的(而对于会话来说,这是强制性的)。 - Denis Otkidach

1
我会从一个线程唯一的ID开始,然后(以某种方式)将其与线程本地计数器连接起来,然后通过加密哈希算法进行处理。

虽然有生成GUID的模块,但哈希技术仍然是一个不错的选择。 - Matthieu M.
多进程怎么办?(WSGIDaemonProcess processes=2 threads=5)无法创建唯一的线程ID? 我认为线程锁是唯一的解决方案? - Gert Cuykens
你可以使用另一个状态(唯一进程ID)并将其与线程唯一ID和线程本地计数器连接起来,然后进行哈希。只要您处于“不是大量线程和进程”的领域,这就有效,因为您可能不想为计数器爆炸64位,但这会给您一个线程本地计数器为32位,然后16位用于进程ID,另外16位用于线程ID,因此在合理的空间范围内。 - Vatine

0

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

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

SELECT 不会创建锁定,因此它将以与在 BEGIN TRANS 之前选择相同的方式工作。 - j.a.estevan
不行。想象一下当两个线程同时执行相同sid的SELECT并收到False时会发生什么。你将有两个插入语句一个接一个。将单个语句包装在事务中没有任何意义。 - yk4ever

0

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

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

会不会对会话 ID 的磁盘使用量太大了? - Gert Cuykens
不行,在返回之前请查看os.rmdir。 有人必须做这项工作...在我的例子中,它只是内核操作系统。 任何其他实现,如DB等,都会在其上添加层。 无论如何,我正在考虑会话管理,需要一个目录。 - Luca
删除目录可能会导致冲突?当然,这需要调查并可能与操作系统/glibc有关。您可以在不再需要uuid的情况下保留目录并进行删除,例如会话注销/超时。有人必须保存数据...这只是我的示例中的操作系统文件系统。正如您所看到的,我总是在考虑会话,我的错;-) - Luca

0

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

过去,当我做这类事情时,通常线程中会有一些独特的东西。例如用户名或客户端名称之类的东西。对我来说,解决方案是将用户名(例如)和当前毫秒时间连接起来,然后对该字符串进行哈希处理并获取哈希的十六进制摘要。这样可以得到一个长度始终相同的漂亮字符串。

有一种非常遥远的可能性,即在两个不同的线程中,两个不同的John Smith(或其他人)在同一毫秒内生成相同的ID。如果这种可能性让人感到紧张,那么可能需要采用锁定路线。

正如已经提到的,已经有例程可以获取GUID。我个人喜欢玩弄哈希函数,因此我已经成功地使用了上述方法在大型多线程系统中自己编写了哈希函数。

最终,你需要决定是否真的有具有重复数据的线程。一定要选择一个好的哈希算法。我已经成功地使用了md5,但我已经读到过可能会生成md5哈希冲突的情况,尽管我从未遇到过。最近我一直在使用sha1。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接