web-dev-qa-db-fra.com

Python: concurrent.futures Comment le rendre annulable?

Python concurrent.futures et ProcessPoolExecutor fournissent une interface soignée pour planifier et surveiller les tâches. Futures even provide une méthode .cancel ():

cancel () : tentez d'annuler l'appel. Si l'appel est en cours d'exécution et ne peut pas être annulé alors la méthode retournera False, sinon l'appel sera annulé et la méthode retournera True.

Malheureusement dans un simmilar question (concernant asyncio) la réponse affirme que les tâches en cours d'exécution ne peuvent pas être annulées en utilisant cette section de la documentation, mais les documents ne le disent pas, seulement s'ils sont en cours d'exécution ET non annulables.

Soumettre le multiprocessing.Events aux processus n'est pas non plus trivialement possible (le faire via des paramètres comme dans multiprocess.Process renvoie un RuntimeError)

Qu'est-ce que j'essaye de faire? Je voudrais partitionner un espace de recherche et exécuter une tâche pour chaque partition. Mais il suffit d'avoir UNE solution et le processus est gourmand en CPU. Existe-t-il donc un moyen réellement confortable d'accomplir cela qui ne compense pas les gains en utilisant ProcessPool pour commencer?

Exemple:

from concurrent.futures import ProcessPoolExecutor, FIRST_COMPLETED, wait

# function that profits from partitioned search space
def m_run(partition):
    for elem in partition:
        if elem == 135135515:
            return elem
    return False

futures = []
# used to create the partitions
steps = 100000000
with ProcessPoolExecutor(max_workers=4) as pool:
    for i in range(4):
        # run 4 tasks with a partition, but only *one* solution is needed
        partition = range(i*steps,(i+1)*steps)
        futures.append(pool.submit(m_run, partition))

    done, not_done = wait(futures, return_when=FIRST_COMPLETED)
    for d in done:
        print(d.result())

    print("---")
    for d in not_done:
        # will return false for Cancel and Result for all futures
        print("Cancel: "+str(d.cancel()))
        print("Result: "+str(d.result()))
15
Ketzu

Je ne sais pas pourquoi concurrent.futures.Future N'a pas de méthode .kill(), mais vous pouvez accomplir ce que vous voulez en fermant le pool de processus avec pool.shutdown(wait=False) et en tuant le processus enfant restant à la main.

Créez une fonction pour tuer les processus enfants:

import signal, psutil

def kill_child_processes(parent_pid, sig=signal.SIGTERM):
    try:
        parent = psutil.Process(parent_pid)
    except psutil.NoSuchProcess:
        return
    children = parent.children(recursive=True)
    for process in children:
        process.send_signal(sig)

Exécutez votre code jusqu'à ce que vous obteniez le premier résultat, puis supprimez tous les processus enfants restants:

from concurrent.futures import ProcessPoolExecutor, FIRST_COMPLETED, wait

# function that profits from partitioned search space
def m_run(partition):
    for elem in partition:
        if elem == 135135515:
            return elem
    return False

futures = []
# used to create the partitions
steps = 100000000
pool = ProcessPoolExecutor(max_workers=4)
for i in range(4):
    # run 4 tasks with a partition, but only *one* solution is needed
    partition = range(i*steps,(i+1)*steps)
    futures.append(pool.submit(m_run, partition))

done, not_done = wait(futures, timeout=3600, return_when=FIRST_COMPLETED)

# Shut down pool
pool.shutdown(wait=False)

# Kill remaining child processes
kill_child_processes(os.getpid())
7
ostrokach

Malheureusement, l'exécution de Futures ne peut pas être annulée. Je crois que la raison principale est d'assurer la même API sur différentes implémentations (il n'est pas possible d'interrompre les threads ou les coroutines en cours d'exécution).

La bibliothèque Pebble a été conçue pour surmonter cette limitation et d'autres.

from pebble import ProcessPool

def function(foo, bar=0):
    return foo + bar

with ProcessPool() as pool:
    future = pool.schedule(function, args=[1])

    # if running, the container process will be terminated 
    # a new process will be started consuming the next task
    future.cancel()  
7
noxdafox

J'ai trouvé votre question intéressante alors voici ma conclusion.

J'ai trouvé que le comportement de la méthode .cancel() est tel qu'indiqué dans la documentation python. Quant à vos fonctions simultanées en cours d'exécution, elles ne pouvaient malheureusement pas être annulées même après qu'on leur avait dit de le faire Si ma conclusion est correcte, alors je raisonne que Python nécessite une méthode .cancel () plus efficace.

Exécutez le code ci-dessous pour vérifier ma conclusion.

from concurrent.futures import ProcessPoolExecutor, as_completed
from time import time 

# function that profits from partitioned search space
def m_run(partition):
    for elem in partition:
        if elem == 3351355150:
            return elem
            break #Added to terminate loop once found
    return False

start = time()
futures = []
# used to create the partitions
steps = 1000000000
with ProcessPoolExecutor(max_workers=4) as pool:
    for i in range(4):
        # run 4 tasks with a partition, but only *one* solution is needed
        partition = range(i*steps,(i+1)*steps)
        futures.append(pool.submit(m_run, partition))

    ### New Code: Start ### 
    for f in as_completed(futures):
        print(f.result())
        if f.result():
            print('break')
            break

    for f in futures:
        print(f, 'running?',f.running())
        if f.running():
            f.cancel()
            print('Cancelled? ',f.cancelled())

    print('New Instruction Ended at = ', time()-start )
print('Total Compute Time = ', time()-start )

pdate: Il est possible d'arrêter de force les processus simultanés via bash, mais la conséquence est que le programme principal python se terminera également. Si ce n'est pas un problème avec vous, puis essayez le code ci-dessous.

Vous devez ajouter les codes ci-dessous entre les 2 dernières instructions d'impression pour le voir par vous-même. Remarque: ce code ne fonctionne que si vous n'exécutez aucun autre programme python3.

import subprocess, os, signal 
result = subprocess.run(['ps', '-C', 'python3', '-o', 'pid='],
                        stdout=subprocess.PIPE).stdout.decode('utf-8').split()
print ('result =', result)
for i in result:
    print('PID = ', i)
    if i != result[0]:
        os.kill(int(i), signal.SIGKILL)
        try: 
           os.kill(int(i), 0)
           raise Exception("""wasn't able to kill the process 
                              HINT:use signal.SIGKILL or signal.SIGABORT""")
        except OSError as ex:
           continue
2
Sun Bear