Cómo hacer que todos los procesos pool.apply_async se detengan una vez que cualquier proceso haya encontrado una coincidencia en python

Tengo el siguiente código que aprovecha el multiprocesamiento para recorrer una lista grande y encontrar una coincidencia. ¿Cómo puedo hacer que todos los procesos se detengan una vez que se encuentra una coincidencia en cualquiera de los procesos? He visto ejemplos, pero ninguno de ellos parece encajar en lo que estoy haciendo aquí.

#!/usr/bin/env python3.5
import sys, itertools, multiprocessing, functools

alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12234567890!@#$%^&*?,()-=+[]/;"
num_parts = 4
part_size = len(alphabet) // num_parts

def do_job(first_bits):
    for x in itertools.product(first_bits, *itertools.repeat(alphabet, num_parts-1)):
        # CHECK FOR MATCH HERE
        print(''.join(x))
        # EXIT ALL PROCESSES IF MATCH FOUND

if __name__ == '__main__':
    pool = multiprocessing.Pool(processes=4)
    results = []

    for i in range(num_parts):
        if i == num_parts - 1:
            first_bit = alphabet[part_size * i :]
        else:
            first_bit = alphabet[part_size * i : part_size * (i+1)]
        pool.apply_async(do_job, (first_bit,))

    pool.close()
    pool.join()

Gracias por tu tiempo.

ACTUALIZACIÓN 1:

He implementado los cambios sugeridos en el gran enfoque de @ShadowRanger y casi está funcionando como yo quiero. Así que he agregado algunos registros para dar una indicación del progreso y poner una clave de 'prueba' para que coincida. Quiero poder aumentar / disminuir los iNumberOfProcessors independientemente de las num_parts. En esta etapa, cuando los tengo a ambos en 4, todo funciona como se esperaba, 4 procesos giran (uno extra para la consola). Cuando cambio los iNumberOfProcessors = 6, 6 procesos giran pero solo algunos de ellos tienen uso de CPU. Entonces parece que 2 están inactivos. Mientras que como mi solución anterior anterior, pude establecer el número de núcleos más alto sin aumentar las num_parts, y todos los procesos se usarían.

No estoy seguro de cómo refactorizar este nuevo enfoque para darme la misma funcionalidad. ¿Puede echar un vistazo y darme alguna dirección con la refactorización necesaria para poder establecer iNumberOfProcessors y num_parts independientemente uno del otro y aún tener todos los procesos utilizados?

Aquí está el código actualizado:

#!/usr/bin/env python3.5
import sys, itertools, multiprocessing, functools

alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12234567890!@#$%^&*?,()-=+[]/;"
num_parts = 4
part_size = len(alphabet) // num_parts
iProgressInterval = 10000
iNumberOfProcessors = 6

def do_job(first_bits):
    iAttemptNumber = 0
    iLastProgressUpdate = 0
    for x in itertools.product(first_bits, *itertools.repeat(alphabet, num_parts-1)):
        sKey = ''.join(x)
        iAttemptNumber = iAttemptNumber + 1
        if iLastProgressUpdate + iProgressInterval <= iAttemptNumber:
            iLastProgressUpdate = iLastProgressUpdate + iProgressInterval
            print("Attempt#:", iAttemptNumber, "Key:", sKey)
        if sKey == 'test':
            print("KEY FOUND!! Attempt#:", iAttemptNumber, "Key:", sKey)
            return True

def get_part(i):
    if i == num_parts - 1:
        first_bit = alphabet[part_size * i :]
    else:
        first_bit = alphabet[part_size * i : part_size * (i+1)]
    return first_bit

if __name__ == '__main__':
    # with statement with Py3 multiprocessing.Pool terminates when block exits
    with multiprocessing.Pool(processes = iNumberOfProcessors) as pool:

        # Don't need special case for final block; slices can 
        for gotmatch in pool.imap_unordered(do_job, map(get_part, range(num_parts))):
             if gotmatch:
                 break
        else:
             print("No matches found")

ACTUALIZACIÓN 2:

Ok, aquí está mi intento de probar la sugerencia de @noxdafox. He reunido lo siguiente en función del enlace que proporcionó con su sugerencia. Desafortunadamente cuando lo ejecuto me sale el error:

... línea 322, en apply_async raise ValueError ("Grupo no ejecutándose") ValueError: Grupo no ejecutándose

¿Alguien puede darme alguna dirección sobre cómo hacer que esto funcione?

Básicamente, el problema es que mi primer intento fue multiprocesamiento, pero no admitió la cancelación de todos los procesos una vez que se encontró una coincidencia.

Mi segundo intento (basado en la sugerencia de @ShadowRanger) resolvió ese problema, pero rompió la funcionalidad de poder escalar el número de procesos y el tamaño de num_parts de forma independiente, que es algo que mi primer intento podría hacer.

Mi tercer intento (basado en la sugerencia de @noxdafox), arroja el error descrito anteriormente.

Si alguien me puede dar alguna dirección sobre cómo mantener la funcionalidad de mi primer intento (poder escalar el número de procesos y el tamaño de num_parts de forma independiente), y agregar la funcionalidad de cancelar todos los procesos una vez que se encuentra una coincidencia, sería muy apreciado .

Gracias por tu tiempo.

Aquí está el código de mi tercer intento basado en la sugerencia de @noxdafox:

#!/usr/bin/env python3.5
import sys, itertools, multiprocessing, functools

alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12234567890!@#$%^&*?,()-=+[]/;"
num_parts = 4
part_size = len(alphabet) // num_parts
iProgressInterval = 10000
iNumberOfProcessors = 4


def find_match(first_bits):
    iAttemptNumber = 0
    iLastProgressUpdate = 0
    for x in itertools.product(first_bits, *itertools.repeat(alphabet, num_parts-1)):
        sKey = ''.join(x)
        iAttemptNumber = iAttemptNumber + 1
        if iLastProgressUpdate + iProgressInterval <= iAttemptNumber:
            iLastProgressUpdate = iLastProgressUpdate + iProgressInterval
            print("Attempt#:", iAttemptNumber, "Key:", sKey)
        if sKey == 'test':
            print("KEY FOUND!! Attempt#:", iAttemptNumber, "Key:", sKey)
            return True

def get_part(i):
    if i == num_parts - 1:
        first_bit = alphabet[part_size * i :]
    else:
        first_bit = alphabet[part_size * i : part_size * (i+1)]
    return first_bit

def grouper(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return itertools.zip_longest(*args, fillvalue=fillvalue)

class Worker():

    def __init__(self, workers):
        self.workers = workers

    def callback(self, result):
        if result:
            self.pool.terminate()

    def do_job(self):
        print(self.workers)
        pool = multiprocessing.Pool(processes=self.workers)
        for part in grouper(alphabet, part_size):
            pool.apply_async(do_job, (part,), callback=self.callback)
        pool.close()
        pool.join()
        print("All Jobs Queued")

if __name__ == '__main__':
    w = Worker(4)
    w.do_job()

Respuestas a la pregunta(2)

Su respuesta a la pregunta