Como fazer com que todos os processos pool.apply_async parem quando um processo encontra uma correspondência em python

Eu tenho o código a seguir que está aproveitando o multiprocessamento para percorrer uma lista grande e encontrar uma correspondência. Como posso interromper todos os processos depois que uma correspondência é encontrada em qualquer um dos processos? Já vi exemplos, mas nenhum deles parece se encaixar no que estou fazendo aqui.

#!/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()

Obrigado pelo seu tempo.

ATUALIZAÇÃO 1:

Eu implementei as mudanças sugeridas na ótima abordagem do @ShadowRanger e ele está quase funcionando da maneira que eu quero. Então, adicionei alguns registros para dar uma indicação do progresso e colocar uma chave 'test' lá para combinar. Eu quero poder aumentar / diminuir o iNumberOfProcessors independentemente do num_parts. Nesta fase, quando eu tenho os dois no 4, tudo funciona como esperado, 4 processos são ativados (um extra para o console). Quando altero o processo iNumberOfProcessors = 6, 6, os processos são ativados, mas apenas um deles possui uso de CPU. Portanto, parece que 2 estão ociosos. Onde, como minha solução anterior acima, eu era capaz de definir o número de núcleos mais alto sem aumentar o num_parts, e todos os processos seriam usados.

Não tenho certeza de como refatorar essa nova abordagem para me fornecer a mesma funcionalidade. Você pode dar uma olhada e me dar alguma orientação com a refatoração necessária para poder definir iNumberOfProcessors e num_parts independentemente um do outro e ainda ter todos os processos usados?

Aqui está o código atualizado:

#!/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")

ATUALIZAÇÃO 2:

Ok, aqui está minha tentativa de tentar a sugestão do @noxdafox. Reuni o seguinte com base no link que ele forneceu com sua sugestão. Infelizmente, quando o executo, recebo o erro:

... linha 322, em apply_async, eleva ValueError ("Pool não está executando") ValueError: pool não está executando

Alguém pode me dar alguma orientação sobre como fazer isso funcionar.

Basicamente, o problema é que minha primeira tentativa fez o multiprocessamento, mas não deu suporte ao cancelamento de todos os processos depois que uma correspondência foi encontrada.

Minha segunda tentativa (com base na sugestão @ShadowRanger) resolveu esse problema, mas quebrou a funcionalidade de poder escalar o número de processos e o tamanho de num_parts independentemente, o que é algo que minha primeira tentativa poderia fazer.

Minha terceira tentativa (com base na sugestão @noxdafox) lança o erro descrito acima.

Se alguém puder me dar alguma orientação sobre como manter a funcionalidade da minha primeira tentativa (poder escalar o número de processos e o tamanho de num_parts independentemente) e adicionar a funcionalidade de cancelar todos os processos assim que uma correspondência for encontrada, seria muito apreciado .

Obrigado pelo seu tempo.

Aqui está o código da minha terceira tentativa, com base na sugestão @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()

questionAnswers(2)

yourAnswerToTheQuestion