如何在Python 2.6中实现线程安全的打印?

56 投票
4 回答
49621 浏览
提问于 2025-04-15 23:53

在Python中,print这个功能在多线程环境下是不安全的,具体可以参考这些 文章

第二篇文章中提供了一种在Python 3中解决这个问题的方法。

那么,在Python 2.6中,如何才能让print变得线程安全呢?

4 个回答

23

问题在于,Python 对于打印换行和打印对象本身使用了不同的操作码。最简单的解决办法可能就是直接使用 sys.stdout.write,并且手动加上换行符。

27

通过实验,我发现以下方法有效、简单,并且满足我的需求:

print "your string here\n",

或者,把它放在一个函数里:

def safe_print(content):
    print "{0}\n".format(content),

我理解的是,普通的 print 语句在输出时会自动加一个换行符,这个换行符实际上是单独处理的,这就可能导致和其他 print 操作之间出现竞争条件。通过在 print 语句后面加上 , 来去掉这个自动换行符,而是把换行符放在字符串里,我们就能避免这个问题。


2020年更新:这是 Python 3 的版本(感谢评论区的 Bob Stein 提供的灵感):

def safe_print(*args, sep=" ", end="", **kwargs):
    joined_string = sep.join([ str(arg) for arg in args ])
    print(joined_string  + "\n", sep=sep, end=end, **kwargs)

正如 Bob Stein 指出的,依赖 print 来连接多个传入的参数会导致输出混乱,所以我们得自己处理这个问题。


2017年更新:这个回答开始受到关注,所以我想做个澄清。这并不意味着 print 完全“线程安全”。如果多个 print 之间的时间间隔只有微秒,输出可能会出现顺序错误。不过,这个方法确实能避免来自并发线程的 print 语句输出混乱,这正是大多数人提问时想要解决的问题。

下面是一个测试,来说明我的意思:

from concurrent.futures import ThreadPoolExecutor


def normal_print(content):
    print content

def safe_print(content):
    print "{0}\n".format(content),


with ThreadPoolExecutor(max_workers=10) as executor:
    print "Normal Print:"
    for i in range(10):
        executor.submit(normal_print, i)

print "---"

with ThreadPoolExecutor(max_workers=10) as executor:
    print "Safe Print:"
    for i in range(10):
        executor.submit(safe_print, i)

输出:

Normal Print:
0
1
23

4
65

7
 9
8
----
Safe Print:
1
0
3
2
4
5
6
7
8
9
42

这是个有趣的问题——在处理一个 print 语句时,涉及到很多事情,比如设置和检查 softspace 属性。让它变得“线程安全”(也就是说:一个线程在打印时,只有在打印换行符的时候才会把“标准输出的控制权”交给另一个线程,这样每一整行的输出都能确保来自同一个线程)是个挑战。通常,处理真正的线程安全的方法是让一个单独的线程专门“拥有”和处理 sys.stdout,通过 Queue.Queue 和它沟通,但这在这里并不太有用,因为问题并不是线程安全[[即使是普通的 print 也没有崩溃的风险,最终输出的字符正是被打印的内容]],而是需要在多个线程之间进行互斥,以便进行更广泛的操作。

所以,我想我做到了……:

import random
import sys
import thread
import threading
import time

def wait():
  time.sleep(random.random())
  return 'W'

def targ():
  for n in range(8):
    wait()
    print 'Thr', wait(), thread.get_ident(), wait(), 'at', wait(), n

tls = threading.local()

class ThreadSafeFile(object):
  def __init__(self, f):
    self.f = f
    self.lock = threading.RLock()
    self.nesting = 0

  def _getlock(self):
    self.lock.acquire()
    self.nesting += 1

  def _droplock(self):
    nesting = self.nesting
    self.nesting = 0
    for i in range(nesting):
      self.lock.release()

  def __getattr__(self, name):
    if name == 'softspace':
      return tls.softspace
    else:
      raise AttributeError(name)

  def __setattr__(self, name, value):
    if name == 'softspace':
      tls.softspace = value
    else:
      return object.__setattr__(self, name, value)

  def write(self, data):
    self._getlock()
    self.f.write(data)
    if data == '\n':
      self._droplock()

# comment the following statement out to get guaranteed chaos;-)
sys.stdout = ThreadSafeFile(sys.stdout)

thrs = []
for i in range(8):
  thrs.append(threading.Thread(target=targ))
print 'Starting'
for t in thrs:
  t.start()
for t in thrs:
  t.join()
print 'Done'

调用 wait 的目的是为了在没有互斥保证的情况下,确保输出是混乱的(因此有了这个注释)。通过包装,也就是上面的代码完全如它所示,并且(至少)在 Python 2.5 及以上版本中(我相信在更早的版本中也能运行,但我手头没有可以轻易检查的版本),输出是:

Thr W -1340583936 W at W 0
Thr W -1340051456 W at W 0
Thr W -1338986496 W at W 0
Thr W -1341116416 W at W 0
Thr W -1337921536 W at W 0
Thr W -1341648896 W at W 0
Thr W -1338454016 W at W 0
Thr W -1339518976 W at W 0
Thr W -1340583936 W at W 1
Thr W -1340051456 W at W 1
Thr W -1338986496 W at W 1
  ...more of the same...

这种“序列化”的效果(线程看起来像是“很好地轮流”输出)是因为当前打印的线程比其他线程慢得多(因为有很多等待!)。如果把 wait 中的 time.sleep 注释掉,输出就会变成:

Thr W -1341648896 W at W 0
Thr W -1341116416 W at W 0
Thr W -1341648896 W at W 1
Thr W -1340583936 W at W 0
Thr W -1340051456 W at W 0
Thr W -1341116416 W at W 1
Thr W -1341116416 W at W 2
Thr W -1338986496 W at W 0
  ...more of the same...

也就是说,更典型的“多线程输出”……除了每一行输出都完全来自同一个线程的保证。

当然,一个线程如果执行 print 'ciao',将会一直“拥有”标准输出,直到它最终执行一个没有尾随逗号的打印,其他想要打印的线程可能会等待很久(要如何保证每一行输出都来自同一个线程呢?一种架构是把部分行累积到线程本地存储,而不是直接写入标准输出,只有在收到 \n 时才进行写入……这在与 softspace 设置交错时可能会很微妙,但大概是可行的)。

撰写回答