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

56
在Python中,根据这些 文章print不是线程安全的。
后一篇文章提供了Python 3的解决方法。
如何在Python 2.6中获得线程安全的print

这些文章是关于Python 3的。哪些文章说明Python 2.x中的print不是线程安全的? - Ignacio Vazquez-Abrams
3
Ignacio说:我亲眼所见 :-) 尝试同时启动几个线程并打印到stdout,行将会变得一团糟。 - knorv
第二篇文章 https://bramcohen.livejournal.com/70686.html 似乎只是使用了一个锁,我认为它与 Python 3 没有任何特定的关系。 - Ciro Santilli OurBigBook.com
4个回答

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 设置正确交错在一起,但可能可行)。


27

通过尝试,我发现以下方法可行,简单易行,并且符合我的需求:

print "your string here\n",

或者,包装在一个函数中:

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

我的理解是,普通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

如果你在bash命令行的末尾加上&,将python脚本发送到后台,然后退出bash shell,当线程遇到这个特别制作的print语句时,将会崩溃并显示"IOError: [Errno 5] Input/output error"。在将作业发送到后台之前,必须将标准输出重定向到某个地方,然后再注销。顺便说一下,我正在使用Python 2.7.12。 - user1748155
顺便说一下,如果我执行普通的打印操作并将程序发送到后台并注销,当Python脚本完成且解释器关闭时(而不是在打印语句出现时),我会遇到这个人描述的问题。https://dev59.com/Kmcs5IYBdhLWcg3wgkSu 因此,在隐式和显式换行符处理标准输出方面存在某些差异。 - user1748155
print("One string with no commas and its own LF\n", end="") 是 Python 3 版本的代码。对我来说,这两个标准使 print() 原子化。但是 print("Separate", "strings", "joined by commas\n", end="") 可能会被拆分。 - Bob Stein
@BobStein 已更新为 Python 3 版本。 - Julien

23

问题在于Python使用不同的操作码来打印NEWLINE和对象本身。最简单的解决方案可能是使用显式的sys.stdout.write和显式的换行符。


8
根据我的最近经验,这是绝对正确的。我不太确定为什么会发生这种情况,但即使 STDOUT 被正确序列化和刷新,print 语句也会输出不稳定的换行符。您 必须 使用 sys.stdout.write(s + '\n') 来避免这种情况。 - efotinis
7
在多线程环境下,仅仅使用sys.stdout.write并不能保证输出的串行化。你还需要添加一个锁(Lock)来确保输出的正确性。 - Pierre-Luc Paour

13
我不知道是否有更好的方式代替这种锁机制,但至少它看起来很容易。我也不确定打印是否真的不是线程安全的。
编辑:好的,我现在已经测试过了,你是对的,输出可能会非常奇怪。而且你不需要导入future模块,只是因为我在使用Python 2.7。
from __future__ import print_function
from threading import Lock

print_lock = Lock()
def save_print(*args, **kwargs):
  with print_lock:
    print (*args, **kwargs)

save_print("test", "omg", sep='lol')

@evilpie:我猜你最终还是使用了Python3。例如,在Python 2.7中,“print(“test1”,“test2”)”实际上被执行为“print tuple(“test1”,“test2”)”。因此,“*args,**kwargs”参数不正确,也不同于“print“test1”,“test2””。 - Alex

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