如何在Python 2.6中实现线程安全的打印?
4 个回答
问题在于,Python 对于打印换行和打印对象本身使用了不同的操作码。最简单的解决办法可能就是直接使用 sys.stdout.write,并且手动加上换行符。
通过实验,我发现以下方法有效、简单,并且满足我的需求:
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
这是个有趣的问题——在处理一个 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
设置交错时可能会很微妙,但大概是可行的)。