39 votes

Sur quels cœurs de CPU mes processus Python fonctionnent-ils ?

La mise en place

J'ai écrit un logiciel assez complexe en Python (sur un PC Windows). Mon logiciel démarre essentiellement deux shells d'interprétation Python. Le premier shell démarre (je suppose) lorsque vous double-cliquez sur le fichier main.py dossier. Dans ce shell, d'autres threads sont lancés de la manière suivante :

    # Start TCP_thread
    TCP_thread = threading.Thread(name = 'TCP_loop', target = TCP_loop, args = (TCPsock,))
    TCP_thread.start()

    # Start UDP_thread
    UDP_thread = threading.Thread(name = 'UDP_loop', target = UDP_loop, args = (UDPsock,))
    TCP_thread.start()

Le site Main_thread commence un TCP_thread et un UDP_thread . Bien qu'il s'agisse de fils séparés, ils s'exécutent tous dans un seul shell Python.

Le site Main_thread démarre également un sous-processus. Cela se fait de la manière suivante :

p = subprocess.Popen(['python', mySubprocessPath], shell=True)

D'après la documentation de Python, je comprends que ce sous-processus s'exécute simultanément ( !) dans une session/un shell séparé de l'interpréteur Python. Le site Main_thread dans ce sous-processus est entièrement dédié à mon interface graphique. L'interface graphique démarre un TCP_thread pour toutes ses communications.

Je sais que les choses se compliquent un peu. C'est pourquoi j'ai résumé l'ensemble du dispositif dans cette figure :

enter image description here


J'ai plusieurs questions concernant cette installation. Je vais les énumérer ici :

Question 1 [ Résolu ]

Est-il vrai qu'un interpréteur Python n'utilise qu'un seul cœur de CPU à la fois pour exécuter tous les threads ? En d'autres termes, est-ce que le Python interpreter session 1 (de la figure) exécuter les 3 fils ( Main_thread , TCP_thread y UDP_thread ) sur un seul cœur de CPU ?

Réponse : oui, c'est vrai. Le GIL (Global Interpreter Lock) garantit que tous les threads fonctionnent sur un seul cœur de processeur à la fois.

Question 2 [ Pas encore résolu ]

Est-ce que j'ai un moyen de savoir de quel cœur de CPU il s'agit ?

Question 3 [ Partiellement résolu ]

Pour cette question, nous oublions fils mais nous nous concentrons sur le sous-processus en Python. Le démarrage d'un nouveau sous-processus implique le démarrage d'un nouvel interpréteur Python instance . Est-ce correct ?

Réponse : Oui, c'est exact. Au début, il y avait une certaine confusion quant à savoir si le code suivant créerait une nouvelle instance de l'interpréteur Python :

    p = subprocess.Popen(['python', mySubprocessPath], shell = True)

La question a été clarifiée. Ce code démarre effectivement une nouvelle instance de l'interpréteur Python.

Python sera-t-il assez intelligent pour que cette instance distincte de l'interpréteur Python s'exécute sur un cœur de CPU différent ? Y a-t-il un moyen de savoir lequel, peut-être avec des instructions print sporadiques ?

Question 4 [ Nouvelle question ]

La discussion communautaire a soulevé une nouvelle question. Il existe apparemment deux approches lors du lancement d'un nouveau processus (dans une nouvelle instance de l'interpréteur Python) :

    # Approach 1(a)
    p = subprocess.Popen(['python', mySubprocessPath], shell = True)

    # Approach 1(b) (J.F. Sebastian)
    p = subprocess.Popen([sys.executable, mySubprocessPath])

    # Approach 2
    p = multiprocessing.Process(target=foo, args=(q,))

La deuxième approche présente l'inconvénient évident de ne cibler qu'une fonction - alors que je dois ouvrir un nouveau script Python. Quoi qu'il en soit, les deux approches sont-elles similaires dans ce qu'elles réalisent ?

25voto

J.F. Sebastian Points 102961

Q : Est-il vrai qu'un interprète Python n'utilise qu'un seul cœur de processeur à la fois pour exécuter tous les threads ?

Non. Le GIL et l'affinité avec le CPU sont des concepts sans rapport. La GIL peut être libérée pendant les opérations d'E/S bloquantes, les longs calculs intensifs du CPU dans une extension C de toute façon.

Si un thread est bloqué sur la GIL, il n'est probablement pas sur un cœur de CPU et il est donc juste de dire que le code multithreading Python pur peut utiliser un seul cœur de CPU à la fois sur la mise en œuvre de CPython.

Q : En d'autres termes, l'interpréteur Python session 1 (de la figure) pourra-t-il exécuter les 3 threads (Main_thread, TCP_thread et UDP_thread) sur un seul cœur de CPU ?

Je ne pense pas que CPython gère l'affinité avec le CPU de manière implicite. Il s'appuie probablement sur le planificateur du système d'exploitation pour choisir où exécuter un thread. Les threads de Python sont implémentés au-dessus des threads réels du système d'exploitation.

Q : Ou l'interpréteur Python est-il capable de les répartir sur plusieurs cœurs ?

Pour connaître le nombre de CPU utilisables :

>>> import os
>>> len(os.sched_getaffinity(0))
16

Encore une fois, le fait que les threads soient planifiés ou non sur différents CPU ne dépend pas de l'interpréteur Python.

Q : Supposons que la réponse à la question 1 soit "plusieurs cœurs", ai-je un moyen de savoir sur quel cœur chaque thread s'exécute, peut-être avec quelques instructions d'impression sporadiques ? Si la réponse à la question 1 est "un seul cœur", ai-je un moyen de savoir de quel cœur il s'agit ?

J'imagine qu'une unité centrale spécifique peut changer d'un créneau horaire à l'autre. Vous pourriez regardez quelque chose comme /proc/<pid>/task/<tid>/status sur les vieux noyaux Linux . Sur ma machine, task_cpu peut être lu à partir de /proc/<pid>/stat ou /proc/<pid>/task/<tid>/stat :

>>> open("/proc/{pid}/stat".format(pid=os.getpid()), 'rb').read().split()[-14]
'4'

Pour une solution portable actuelle, voir si psutil expose de telles informations.

Vous pourriez restreindre le processus en cours à un ensemble de CPU :

os.sched_setaffinity(0, {0}) # current process on 0-th core

Q : Pour cette question, nous oublions les threads, mais nous nous concentrons sur le mécanisme des sous-processus dans Python. Le démarrage d'un nouveau sous-processus implique le démarrage d'une nouvelle session/shell de l'interpréteur Python. Est-ce correct ?

Oui. subprocess crée de nouveaux processus OS. Si vous exécutez python exécutable alors il démarre un nouvel interprète Python. Si vous exécutez un bash script alors aucun nouvel interpréteur Python n'est créé, c'est-à-dire que l'exécution de bash ne démarre pas un nouvel interpréteur/session/etc. Python.

Q : En supposant que cela soit correct, Python sera-t-il assez intelligent pour que cette session d'interprétation séparée s'exécute sur un cœur de CPU différent ? Y a-t-il un moyen de suivre cela, peut-être avec quelques instructions print sporadiques ?

Voir ci-dessus (c'est-à-dire que le système d'exploitation décide où exécuter votre thread et il pourrait y avoir une API du système d'exploitation qui expose l'endroit où le thread est exécuté).

multiprocessing.Process(target=foo, args=(q,)).start()

multiprocessing.Process crée également un nouveau processus OS (qui exécute un nouvel interpréteur Python).

En réalité, mon sous-processus est un autre fichier. Donc cet exemple ne fonctionnera pas pour moi.

Python utilise des modules pour organiser le code. Si votre code est dans another_file.py puis import another_file dans votre module principal et passez another_file.foo à multiprocessing.Process .

Néanmoins, comment le compareriez-vous à p = subprocess.Popen(..) ? Est-il important de lancer le nouveau processus (ou devrais-je dire 'instance de l'interpréteur python') avec subprocess.Popen(..)ou multiprocessing.Process(..) ?

multiprocessing.Process() est probablement mis en œuvre au-dessus de subprocess.Popen() . multiprocessing fournit une API qui est similaire à threading et il fait abstraction des détails de la communication entre les processus Python (comment les objets Python sont sérialisés pour être envoyés entre les processus).

S'il n'y a pas de tâches intensives pour le CPU, vous pouvez exécuter vos threads d'interface graphique et d'E/S dans un seul processus. Si vous avez une série de tâches gourmandes en ressources CPU, pour utiliser plusieurs CPU à la fois, vous pouvez soit utiliser plusieurs threads avec des extensions C telles que lxml , regex , numpy (ou votre propre modèle créé à l'aide de Cython ) qui peut libérer GIL pendant les longs calculs ou les décharger dans des processus séparés (une manière simple est d'utiliser un pool de processus tel que fourni par concurrent.futures ).

Q : La discussion communautaire a soulevé une nouvelle question. Il existe apparemment deux approches lors du lancement d'un nouveau processus (dans une nouvelle instance de l'interpréteur Python) :

# Approach 1(a)
p = subprocess.Popen(['python', mySubprocessPath], shell = True)

# Approach 1(b) (J.F. Sebastian)
p = subprocess.Popen([sys.executable, mySubprocessPath])

# Approach 2
p = multiprocessing.Process(target=foo, args=(q,))

"Approche 1(a)" est erronée sous POSIX (bien qu'elle puisse fonctionner sous Windows). Pour la portabilité, utilisez "Approche 1(b)" sauf si vous savez que vous avez besoin cmd.exe (passez une chaîne dans ce cas, pour vous assurer que l'échappement correct de la ligne de commande est utilisé).

La seconde approche présente l'inconvénient évident de ne cibler qu'une fonction - alors que je dois ouvrir un nouveau script Python. Quoi qu'il en soit, les deux approches sont-elles similaires dans ce qu'elles réalisent ?

subprocess crée de nouveaux processus, tout Par exemple, vous pourriez exécuter un bash script. multprocessing est utilisé pour exécuter du code Python dans un autre processus. Il est plus souple de import un module Python et exécuter sa fonction que de l'exécuter en tant que script. Voir Appeler python script avec entrée dans un python script utilisant un sous-processus. .

3voto

gdlmx Points 2500

Puisque vous utilisez le threading qui est construit sur thread . Comme le suggère la documentation, il utilise l'"implémentation POSIX thread". pthread de votre système d'exploitation.

  1. Les threads sont gérés par le système d'exploitation au lieu de l'interpréteur Python. La réponse dépendra donc de la bibliothèque pthread de votre système. Cependant, CPython utilise la GIL pour empêcher plusieurs threads d'exécuter des bytecodes Python de manière simultanée. Ils seront donc séquentialisés. Mais ils peuvent toujours être séparés sur différents cœurs, ce qui dépend de votre bibliothèque pthread.
  2. Il suffit d'utiliser un débogueur et de l'attacher à votre python.exe. Par exemple, le Commande thread de GDB .
  3. Comme pour la question 1, le nouveau processus est géré par votre système d'exploitation et fonctionne probablement sur un noyau différent. Utilisez le débogueur ou tout autre moniteur de processus pour le voir. Pour plus de détails, allez à la page CreatProcess() documentation page .

1voto

robyschek Points 1565

1, 2 : Vous avez trois threads réels, mais dans CPython ils sont limités par la GIL , donc, en supposant qu'ils exécutent du python pur, vous verrez l'utilisation du CPU comme si un seul cœur était utilisé.

3 : Comme l'a dit gdlmx, c'est au système d'exploitation de choisir le noyau sur lequel exécuter un thread, mais si vous avez vraiment besoin de contrôle, vous pouvez définir l'affinité du processus ou du thread en utilisant l'API native via ctypes . Comme vous êtes sous Windows, ce serait comme ceci :

# This will run your subprocess on core#0 only
p = subprocess.Popen(['python', mySubprocessPath], shell = True)
cpu_mask = 1
ctypes.windll.kernel32.SetProcessAffinityMask(p._handle, cpu_mask)

J'utilise ici le privé Popen._handle pour plus de simplicité. La méthode propre serait OpenProcess(p.tid) etc.

Et oui, subprocess exécute python comme tout le reste dans un autre nouveau processus.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X