Python的线程/多进程不需要互斥锁吗?

9
对于类似于监管项目的工程,我使用线程库来管理一些子进程。在某些时候,用户可以提示命令以向处理管理线程发送指令。这些命令存储在主进程和处理管理线程之间共享的队列对象中。我认为我需要互斥锁来解决并发问题,所以我先写了一个小脚本来尝试它,但首先没有使用互斥锁,以确保我得到预期的并发问题。
我希望从脚本中输出每秒钟混乱的整数列表:
import threading
import time

def longer(l, mutex=None):
    while 1:
        last_val = l[-1]
        l.append(last_val + 1)
        time.sleep(1)
    return

dalist = [0]
t = threading.Thread(target=longer, args=(dalist,))
t.daemon = True
t.start()

while 1:
    last_val = dalist[-1]
    dalist.append(last_val + 1)
    print dalist
    time.sleep(1)

但实际上它会打印出以下整数的漂亮列表,就像这样:
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4, 5, 6]

根据另一篇帖子中this的答案,我认为它来自线程库,因此我使用了同样的方法使用了多进程库:

import multiprocessing as mp
import time

def longer(l, mutex=None):
    while 1:
        last_val = l[-1]
        l.append(last_val + 1)
        time.sleep(1)
    return

dalist = [0]
t = mp.Process(target=longer, args=(dalist,))
t.start()

while 1:
    last_val = dalist[-1]
    dalist.append(last_val + 1)
    print dalist
    time.sleep(1)

但我得到了相同的结果,有点“慢”:
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]

我想知道在线程之间共享一个类似于队列的对象时,是否真的需要互斥量来管理?另外,从上面的代码中,我该如何有效地重现我正在寻找的并发问题?
感谢阅读。
编辑1: 根据user4815162342的评论,我更改了第一个代码片段,并通过将睡眠调用移动到“较长”函数中的值检索和列表附加之间,成功产生了某种竞态条件。
import threading
import time

def longer(l, mutex=None):
    while 1:
        last_val = l[-1]
        time.sleep(1)
        l.append(last_val + 1)
    return 

dalist = [0]
t = threading.Thread(target=longer, args=(dalist,))
t.daemon = True
t.start()

while 1:
    last_val = dalist[-1]
    dalist.append(last_val + 1)
    print dalist
    time.sleep(1)

这给我提供了像这样的东西:

[0, 1]
[0, 1, 1, 2]
[0, 1, 1, 2, 2, 3]
[0, 1, 1, 2, 2, 3, 3, 4]

我成功地使用线程锁来解决了我的人工问题,方法如下:

import threading
import time

def longer(l, mutex=None):
    while 1:
        if mutex is not None:
            mutex.acquire()
        last_val = l[-1]
        time.sleep(1)
        l.append(last_val + 1)
        if mutex is not None:
            mutex.release()
    return

dalist = [0]
mutex = threading.Lock()
t = threading.Thread(target=longer, args=(dalist, mutex))
t.daemon = True
t.start()

while 1:
    if mutex is not None:
        mutex.acquire()
    last_val = dalist[-1]
    dalist.append(last_val + 1)
    if mutex is not None:
        mutex.release()
    print dalist
    time.sleep(1)

然后生成:

[0, 1, 2]
[0, 1, 2, 3, 4, 5]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

1
请注意,获取和释放互斥锁的惯用方法是使用“with”语句而不是显式调用“acquire”和“release”方法。此外,很少需要将互斥锁传递给函数 - 保护全局资源的互斥锁将驻留在全局变量中,而保护对象级资源的互斥锁将驻留在对象中,并且可以通过“self.some_attribute”访问。 - user4815162342
谢谢您提供的额外信息 :) 我来自C编程,并学到了全局变量是不好的习惯(安全问题、代码清晰度等),请问在Python中,全局变量是否有特别需要注意的问题呢? - shorty_ponton
1个回答

4

你的第一个代码片段存在竞态条件,需要一个互斥锁。全局解释器锁使得竞争条件很少发生,因为任何给定时间只有一个线程在运行。然而,每隔几个字节码指令,当前线程就会放弃对全局解释器锁的所有权,以便让其他线程有机会运行。所以,考虑到你的代码:

last_val = dalist[-1]
dalist.append(last_val + 1)

如果执行第一行后发生字节码切换,另一个线程将获取相同的last_val并将其附加到列表中。在将控制返回给初始线程后,存储在last_val中的值将被附加到列表中,第二次。互斥锁可以以明显的方式防止竞争:在访问列表和附加之间进行上下文切换将控制权交给其他线程,但它会立即被互斥锁阻塞并将控制权还回原始线程。
您的第二个示例仅“工作”,因为两个进程有单独的列表实例。修改一个列表不会影响另一个列表,因此另一个进程可能根本不运行。虽然multiprocessing具有适用于线程的可替代API,但基础概念非常不同,因此需要在从一个API到另一个API进行切换时进行考虑。

好的,所以使用第一个片段,如果我使用多个线程,竞态条件问题可能会更加明显,对吗? - shorty_ponton
@shorty_ponton 是的,还有更短的睡眠时间等等。当然,在 dalist.append(...)之前甚至插入一个微小的睡眠也应该引起它。 - user4815162342

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