Python多线程列表追加操作导致结果出乎意料

7

我想测试从两个线程附加到列表是否可以正常运行,但输出结果很混乱:

import threading


class myThread(threading.Thread):
    def __init__(self, name, alist):
        threading.Thread.__init__(self)
        self.alist = alist

    def run(self):
        print "Starting " + self.name
        append_to_list(self.alist, 2)
        print "Exiting " + self.name
        print self.alist


def append_to_list(alist, counter):
    while counter:
        alist.append(alist[-1]+1)
        counter -= 1

alist = [1, 2]
# Create new threads
thread1 = myThread("Thread-1", alist)
thread2 = myThread("Thread-2", alist)

# Start new Threads
thread1.start()
thread2.start()

print "Exiting Main Thread"
print alist

因此,输出结果为:
Starting Thread-1
Exiting Thread-1
 Starting Thread-2
 Exiting Main Thread
Exiting Thread-2
[1[1, 2[, 1, 2, 23, , 34, 5, 6, ]4
, 5, , 3, 64, 5, ]6]

为什么代码这么混乱,而 alist 却不等于 [1,2,3,4,5,6]?
4个回答

7

摘要

为什么输出结果很乱?

==> 因为一个线程可能会在执行print语句的过程中暂停。

为什么aList不等于[1, 2, 3, 4, 5, 6]?

==> 因为在从aList读取和向其中添加内容之间,aList的内容可能会发生变化。

输出

输出结果混乱是因为它是由Python2的print语句在线程内部生成的,而print语句不是线程安全。这意味着一个线程可能会在print执行时放弃执行权。在问题的代码中,有多个线程在打印,因此一个线程可能在打印时放弃执行权,而另一个线程可能开始打印,然后暂停,从而产生OP看到的交错输出。像写入stdout这样的IO操作在CPU方面非常缓慢,因此很可能操作系统会暂停执行IO操作的线程,因为线程正在等待硬件完成某些操作。
例如,下面的代码:
import threading


def printer():
    for i in range(2):
        print ['foo', 'bar', 'baz']


def main():
    threads = [threading.Thread(target=printer) for x in xrange(2)]
    for t in threads: 
        t.start()
    for t in threads:
        t.join()

生成此交错输出:

>>> main()
['foo', 'bar'['foo', , 'bar', 'baz']
'baz']
['foo', ['foo', 'bar''bar', 'baz']
, 'baz']

使用可以防止交错行为:

def printer():
    for i in range(2):
        with lock:
            print ['foo', 'bar', 'baz']


def main():
    global lock
    lock = threading.Lock()
    threads = [threading.Thread(target=printer) for x in xrange(2)]
    for t in threads: 
        t.start()
    for t in threads:
        t.join()

>>> main()
['foo', 'bar', 'baz']
['foo', 'bar', 'baz']
['foo', 'bar', 'baz']
['foo', 'bar', 'baz']

列表的内容

如果执行语句aList.append(aList[-1] + 1)是原子性的,即当前线程不会让出给另一个正在读取和追加aList的线程,那么aList的最终内容将是[1, 2, 3, 4, 5, 6]

然而,这并不是线程的工作方式。线程可能在从aList读取最后一个元素或增加值后让出,因此很有可能出现以下事件序列:

  1. Thread1从aList中读取值2
  2. Thread1让出执行权
  3. Thread2从aList中读取值2,然后添加3
  4. Thread2从aList中读取值3,然后添加4
  5. Thread2让出执行权
  6. Thread1添加3
  7. Thread1从aList中读取值3,然后添加4

这将使aList变为[1, 2, 3, 4, 3, 4]

与print语句一样,这可以通过在执行aList.append(aList[-1] + 1)之前使线程获取锁来防止。

(注意,list.append 方法在纯 Python 代码中是线程安全的,因此附加的值不会被破坏。)

3

编辑:@kroltan 让我思考了更多,我认为你的示例实际上比我最初想象的线程安全性更高。问题不在于总的多个写入程序线程,而是特别在于这行:

alist.append(alist[-1]+1)

不能保证 append 直接在 alist[-1] 完成后发生,其他操作可能会交错执行。

详细解释请参见:http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm

替换其他对象的操作可能在引用计数为零时调用这些其他对象的 del 方法,并可能影响其他内容。这对于字典和列表的大量更新尤其如此。如果有疑问,请使用互斥锁!

原始回答:

由于您有多个线程写入同一块内存,因此这是未定义的行为,因此您观察到了“凌乱”的输出。

我想测试从两个线程追加到列表是否可以,但我得到了混乱的输出

我认为您已经成功测试了这一点,答案是否定的。 在 Stack Overflow 上有更多详细的解释: https://dev59.com/4VbUa4cB1Zd3GeqPDfeo#5943027


1
它真的是未定义的吗?列表肯定是线程安全的。同时写入stdout肯定是不可预测的,但是有明确定义。 - Kroltan
1
列表是线程安全的。你的第一个陈述是正确的 - 由于在检索alist[-1]和调用append之间进行其他操作,列表数字不会单调增长。但“混乱”的输出是由于打印语句造成的:那个不是线程安全的,两个调用print的输出被搞乱了。然而,生成的列表仍应只包含整数对象。 - jsbueno

0

你需要使用 threading.lock 方法,确保一个线程执行动作(例如将输出打印到屏幕上),不会干扰其他线程的操作。


0

由于您正在使用相同的变量进行读写操作,这将导致未定义的行为。我执行了代码,在同一台机器上的两个不同实例中得到了2个不同的输出:

Starting Thread-1 
Exiting Thread-1 
[1, 2, 3, 4]Starting Thread-2   

Exiting Main Thread 
 [Exiting Thread-21, 2, 3, 4 
, [51, , 62],
3, 4, 5, 6]

而这个

Starting Thread-1
Exiting Thread-1
[1, 2, 3, 4]
Exiting Main Thread
[1, 2, 3, 4]
Starting Thread-2
Exiting Thread-2
[1, 2, 3, 4, 5, 6]

在编程中,你应该使用同步来获得所需的输出,否则就要等待不确定状态才能得到正确的输出。

编辑:你可以阅读这篇文章,了解如何实现同步http://theorangeduck.com/page/synchronized-python


好的,感谢您的回答,但是您能否提供一些实现同步的代码呢? - Alexey

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