web-dev-qa-db-fra.com

Comment puis-je obtenir une impression thread-safe en Python 2.6?

print in Python n'est pas thread-safe selon thesearticles .

Une solution de contournement Python 3 est proposée dans ce dernier article.

Comment puis-je obtenir un thread sécurisé print dans Python 2.6?

49
knorv

Problème intéressant - compte tenu de tout ce qui se passe dans une instruction print, y compris la définition et la vérification de l'attribut softspace, ce qui en fait "threadsafe" (ce qui signifie en fait: un thread qui imprime uniquement cède le "contrôle de la sortie standard" à un autre thread lorsqu'il imprime une nouvelle ligne, de sorte que chaque ligne entière qui sort est garantie de provenir d'un seul thread) était un peu difficile (l'approche facile habituelle de réelle) sécurité des threads - déléguer un thread séparé à "posséder" et gérer exclusivement sys.stdout, communiquez avec elle via Queue.Queue - ce n'est pas très utile, car le problème est pas la sécurité des threads [[même avec un simple print il n'y a aucun risque de planter et les caractères qui se retrouvent sur la sortie standard sont exactement ceux qui s'impriment]] mais le besoin d'exclusion mutuelle entre les threads pour une gamme étendue d'opérations).

Donc, je pense que je l'ai fait ...:

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'

Les appels à wait sont destinés à garantir une sortie chaotique mélangée en l'absence de cette garantie d'exclusion mutuelle (d'où le commentaire). Avec le wrapping, c'est-à-dire le code ci-dessus exactement tel qu'il y apparaît, et (au moins) Python 2.5 et (je pense que cela peut aussi fonctionner dans les versions antérieures, mais je n'ai pas facilement à vérifier) ​​la sortie est:

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...

L'effet de "sérialisation" (où les threads semblent "bien tournés" comme ci-dessus) est un effet secondaire du fait que le thread qui arrive à être en cours d'impression est sérieusement plus lent que les autres (toutes ces attentes! -). Commentant le time.sleep dans wait, la sortie est à la place

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...

c'est-à-dire une "sortie multithread" plus typique ... sauf pour la garantie que chaque ligne de la sortie provient entièrement d'un seul thread.

Bien sûr, un thread qui fait, par exemple, print 'ciao', gardera la "propriété" de la sortie standard jusqu'à ce qu'elle effectue finalement une impression sans virgule de fin, et les autres threads souhaitant imprimer peuvent rester en veille pendant assez longtemps un moment (comment peut-on garantir autrement que chaque ligne dans la sortie provient d'un seul thread? eh bien, une architecture serait d'accumuler des lignes partielles pour stocker le stockage local au lieu de les écrire réellement sur la sortie standard, et de ne faire que l'écriture à la réception du \n... délicat à entrelacer correctement avec softspace paramètres, je le crains, mais probablement faisable).

38
Alex Martelli

Le problème est que python utilise des opcodes séparés pour l'impression NEWLINE et l'impression de l'objet lui-même. La solution la plus simple est probablement d'utiliser simplement un sys.stdout.write explicite avec une nouvelle ligne explicite.

23
Alex Gaynor

Grâce à l'expérimentation, j'ai trouvé que les travaux suivants sont simples et répondent à mes besoins:

print "your string here\n",

Ou, enveloppé dans une fonction,

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

Ma compréhension est que la nouvelle ligne implicite d'un print normal est en fait sortie vers stdout dans une opération distincte, provoquant la condition de concurrence avec d'autres opérations print. En supprimant cette nouvelle ligne implicite avec le ,, et au lieu d'inclure la nouvelle ligne dans la chaîne, nous pouvons éviter ce problème.


2017 Edit: cette réponse commence à prendre de la vapeur, donc je voulais juste apporter une clarification. Cela ne rend pas réellement print "thread-safe" exactement. La sortie peut être dans le mauvais ordre si les print se produisent en microsecondes les unes des autres. Ce que cela fait , cependant, est d'éviter la sortie tronquée provenant des instructions print exécutées à partir de threads simultanés, ce que la plupart des gens veulent vraiment en posant cette question.

Voici un test pour montrer ce que je veux dire:

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)

Production:

Normal Print:
0
1
23

4
65

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

Je ne sais pas s'il y a une meilleure façon à la place de ce mécanisme de verrouillage, mais au moins ça a l'air facile. Je ne sais pas non plus si l'impression n'est pas vraiment sûre pour les threads.

Edit: OK, je l'ai testé moi-même maintenant, vous avez raison, vous pouvez obtenir une sortie vraiment bizarre. Et vous n'avez pas besoin de l'import future, c'est juste là, car j'utilise 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')
13
evilpie