web-dev-qa-db-fra.com

Multitraitement vs Threading Python

J'essaie de comprendre les avantages de multitraitement sur threading . Je sais que le multitraitement contourne le verrou d'interpréteur global, mais quels autres avantages y at-il et que vous pouvez enfiler ne fais pas la même chose?

680
John

Le module threading utilise des threads, le module multiprocessing utilise des processus. La différence est que les threads s'exécutent dans le même espace mémoire, alors que les processus ont une mémoire séparée. Cela rend un peu plus difficile le partage d’objets entre des processus multitraitement. Étant donné que les threads utilisent la même mémoire, des précautions doivent être prises ou deux threads écrivent dans la même mémoire en même temps. C'est à cela que sert le verrou d'interprète global.

Les processus de génération sont un peu plus lents que les threads. Une fois qu'ils fonctionnent, il n'y a pas beaucoup de différence.

604
Sjoerd

Voici quelques avantages/inconvénients que je suis venu avec.

Multitraitement

Avantages

  • Séparer l'espace mémoire
  • Le code est généralement simple
  • Profite de plusieurs processeurs et cœurs
  • Evite les limitations GIL pour cPython
  • Élimine la plupart des besoins en primitives de synchronisation, sauf si vous utilisez la mémoire partagée (il s'agit plutôt d'un modèle de communication pour IPC)
  • Les processus enfants sont interruptibles/destructibles
  • Le module Python multiprocessing inclut des abstractions utiles avec une interface très semblable à threading.Thread
  • Un must avec cPython pour le traitement lié au CPU

Les inconvénients

  • IPC un peu plus compliqué avec plus de temps système (modèle de communication vs. mémoire/objets partagés)
  • Plus grande empreinte mémoire

Filetage

Avantages

  • Léger - faible empreinte mémoire
  • Mémoire partagée - facilite l'accès à l'état depuis un autre contexte
  • Permet de créer facilement des interfaces utilisateur réactives
  • les modules d'extension cPython C qui libèrent correctement le GIL s'exécutent en parallèle
  • Excellente option pour les applications liées aux entrées/sorties

Les inconvénients

  • cPython - soumis au GIL
  • Non interruptible/tuable
  • Si vous ne suivez pas un modèle de file d'attente de commandes/de pompes de messages (à l'aide du module Queue), l'utilisation manuelle des primitives de synchronisation devient une nécessité (des décisions sont nécessaires pour la granularité du verrouillage).
  • Le code est généralement plus difficile à comprendre et à comprendre - le potentiel de conditions de concurrence augmente considérablement
741
Jeremy Brown

Le travail de Threading est de permettre aux applications d'être réactives. Supposons que vous ayez une connexion à une base de données et que vous deviez répondre aux commentaires de l'utilisateur. Sans thread, si la connexion à la base de données est occupée, l'application ne pourra pas répondre à l'utilisateur. En scindant la connexion à la base de données en un thread séparé, vous pouvez rendre l'application plus réactive. De plus, les deux threads étant dans le même processus, ils peuvent accéder aux mêmes structures de données - de bonnes performances, plus une conception logicielle flexible.

Notez qu'en raison de l'application GIL, l'application ne fait pas deux choses à la fois, mais nous avons également placé le verrou de ressource de la base de données dans un thread séparé, de sorte que le temps processeur puisse être commuté entre elle et l'interaction de l'utilisateur. Le temps de calcul est rationné entre les threads.

Le multitraitement est destiné aux moments où vous souhaitez réellement faire plus d’une chose à la fois. Supposons que votre application doive se connecter à 6 bases de données et effectuer une transformation matricielle complexe sur chaque jeu de données. Le fait de placer chaque tâche dans un thread séparé peut aider un peu, car lorsqu'une connexion est inactive, une autre peut générer un peu de temps CPU, mais le traitement ne peut pas être effectué en parallèle, car GIL signifie que vous n'utilisez jamais les ressources d'un seul CPU. . En mettant chaque tâche dans un processus de multitraitement, chacune peut être exécutée sur son propre processeur et être exécutée à pleine efficacité.

191
Simon Hibbs

Le principal avantage est l'isolement. Un processus en panne ne fera pas tomber les autres processus, alors qu'un thread en panne va probablement causer des ravages avec d'autres threads.

39
Marcelo Cantos

Une autre chose qui n’a pas été mentionnée est que cela dépend du système d’exploitation que vous utilisez en termes de vitesse. Dans Windows, les processus sont coûteux et les threads seraient meilleurs dans Windows, mais dans les processus unix, ils sont plus rapides que leurs variantes Windows. L'utilisation des processus dans unix est donc beaucoup plus sûre et rapide.

27
chrissygormley

D’autres réponses ont été plus axées sur les aspects multithreading et multitraitement, mais en python, Global Interpreter Lock (GIL) doit être pris en compte. Quand plus de nombre (disons k) de threads sont créés, ils n'augmenteront généralement pas les performances en k fois, car il sera toujours en cours d'exécution en tant qu'une seule application threadée. GIL est un verrou global qui verrouille tout et ne permet qu'une exécution à un seul thread utilisant un seul cœur. Les performances augmentent effectivement dans les endroits où des extensions C telles que numpy, Réseau, E/S sont utilisées, où beaucoup de travail de fond est effectué et où GIL est publié.
Donc quand filetage est utilisé, il n’existe qu’un seul thread au niveau du système d’exploitation, tandis que python crée des pseudo-threads entièrement gérés par le thread lui-même mais s’exécutant essentiellement en tant que processus unique. La préemption a lieu entre ces pseudo-threads. Si le processeur fonctionne à pleine capacité, vous pouvez passer au multitraitement.
Maintenant, en cas d’instances d’exécution autonomes, vous pouvez opter pour le pool. Toutefois, en cas de chevauchement des données, lorsque vous souhaitez que les processus communiquent, vous devez utiliser multiprocessing.Process.

17
Chitransh Gaurav

citations de la documentation Python

J'ai mis en évidence les citations clés de la documentation Python sur Process vs Threads et GIL à l'adresse suivante: Qu'est-ce que le verrou d'interpréteur global (GIL) dans CPython?

Processus vs expériences de threads

J'ai fait un peu de benchmarking afin de montrer plus concrètement la différence.

Dans le test de performances, j’ai chronométré le processeur et IO travaux liés pour différents nombres de threads sur un processeur 8 hyperthread . Le travail fourni par thread est toujours le même, de sorte que plus de threads signifie plus de travail total fourni.

Les résultats ont été:

enter image description here

données de tracé .

Conclusions:

  • pour le travail lié au processeur, le multitraitement est toujours plus rapide, probablement à cause de la GIL

  • pour IO travail lié. les deux ont exactement la même vitesse

  • les threads ne font que monter à environ 4x au lieu des 8x attendus puisque je suis sur une machine à hyperthread 8.

    Comparez cela avec un travail lié au processeur C POSIX qui atteint l’accélération attendue 8x: Que signifient les expressions "réel", "utilisateur" et "sys" dans la sortie du temps (1)?

    TODO: Je ne connais pas la raison, il doit y avoir d'autres inefficacités de Python.

Code de test:

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    A useless CPU bound function.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __== '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))

GitHub en amont + code de traçage sur le même répertoire .

Testé sur Ubuntu 18.10, Python 3.6.7, sur un ordinateur portable Lenovo ThinkPad P51 avec CPU: CPU Intel Core i7-7820HQ (4 cœurs/8 threads), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HJJQQH 000L7 (3 000 Mo/s).

Comme mentionné dans la question, multitraitement en Python est le seul moyen de parvenir à un véritable parallélisme. Multithreading ne peut y parvenir, car GIL empêche les threads de s'exécuter en parallèle.

Par conséquent, le threading peut ne pas toujours être utile en Python et peut même entraîner une dégradation des performances en fonction de vos objectifs. Par exemple, si vous effectuez une tâche liée au processeur telle que la décompression de fichiers gzip ou le rendu 3D (tout ce qui nécessite beaucoup de temps), le threading peut en réalité gêner votre la performance plutôt que l'aide. Dans un tel cas, vous voudriez utiliser multitraitement car seule cette méthode est exécutée en parallèle et vous aidera à répartir le poids de la tâche à accomplir. Cela pourrait entraîner une surcharge car multitraitement implique la copie de la mémoire d'un script dans chaque sous-processus, ce qui peut entraîner des problèmes pour les applications de grande taille.

Cependant, Multithreading devient utile lorsque votre tâche est liée à l'IO . Par exemple, si l'essentiel de votre tâche implique l'attente des appels d'API , vous devez utiliser Multithreading car pourquoi ne pas en lancer un autre demande dans un autre thread pendant que vous attendez, plutôt que de laisser votre CPU inactif.

TL; DR

  • Multithreading est simultané et utilisé pour les tâches liées à l'IO
  • multitraitement permet d'obtenir un vrai parallélisme et est utilisé pour des tâches liées à la CPU
10
Bolboa

Le processus peut avoir plusieurs threads. Ces threads peuvent partager la mémoire et sont les unités d'exécution au sein d'un processus.

Les processus s'exécutant sur le processeur, les threads résident sous chaque processus. Les processus sont des entités individuelles qui fonctionnent indépendamment. Si vous souhaitez partager des données ou des états entre chaque processus, vous pouvez utiliser un outil de stockage en mémoire tel que Cache(redis, memcache), Files ou __unDatabase.

MULTITRAITEMENT

  • Le multitraitement ajoute des processeurs pour augmenter la puissance de calcul.
  • Plusieurs processus sont exécutés simultanément.
  • La création d'un processus prend beaucoup de temps et consomme beaucoup de ressources.
  • Le multitraitement peut être symétrique ou asymétrique.
  • La bibliothèque de multitraitement en Python utilise un espace mémoire séparé, plusieurs cœurs de processeur, contourne les limitations GIL dans CPython, les processus enfants sont destructibles (par exemple, les appels de fonction dans le programme) et sont beaucoup plus faciles à utiliser.
  • Certaines mises en garde du module impliquent une plus grande empreinte mémoire et les IPC un peu plus compliqués avec davantage de charge.

MULTITHREADING

  • Le multithreading crée plusieurs threads d'un même processus pour augmenter la puissance de calcul.
  • Plusieurs threads d'un même processus sont exécutés simultanément.
  • La création d'un fil est économique en termes de temps et de ressources.
  • La bibliothèque multithreading est légère, partage la mémoire, est responsable de l'interface utilisateur réactive et est bien utilisée pour les applications liées aux E/S.
  • Le module ne peut pas être tué et est soumis à la loi GIL.
  • Plusieurs threads vivent dans le même processus dans le même espace, chaque thread effectuant une tâche spécifique, ayant son propre code, sa propre mémoire de pile, son pointeur d'instruction et son partage de mémoire.
  • Si un thread a une fuite de mémoire, cela peut endommager les autres threads et le processus parent.

Exemple de multi-threading et de multitraitement en Python

Python 3 a la facilité de Lancer des tâches parallèles . Cela facilite notre travail.

Il a pour pool de threads et pool de processus .

Ce qui suit donne un aperçu:

Exemple de ThreadPoolExecutor

import concurrent.futures
import urllib.request

URLS = ['http://www.foxnews.com/',
        'http://www.cnn.com/',
        'http://europe.wsj.com/',
        'http://www.bbc.co.uk/',
        'http://some-made-up-domain.com/']

# Retrieve a single page and report the URL and contents
def load_url(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()

# We can use a with statement to ensure threads are cleaned up promptly
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    # Start the load operations and mark each future with its URL
    future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (url, exc))
        else:
            print('%r page is %d bytes' % (url, len(data)))

ProcessPoolExecutor

import concurrent.futures
import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for number, prime in Zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

if __== '__main__':
    main()
1
Jeril

Comme je l’ai appris à l’université, la plupart des réponses ci-dessus sont exactes. Dans PRATIQUE sur différentes plates-formes (toujours en utilisant python), la création de plusieurs threads aboutit à la création d'un processus. La différence est que les cœurs multiples partagent la charge au lieu d’un seul, traitant à 100%. Donc, si vous créez par exemple 10 threads sur un PC à 4 processeurs, vous n’obtenez que 25% de la puissance du processeur! Et si vous créez 10 processus, vous vous retrouverez avec le traitement du processeur à 100% (si vous n’avez pas d’autres limitations). Je ne suis pas un expert dans toutes les nouvelles technologies. Je réponds avec une expérience réelle

0
Alex