concurrent.futures.ThreadPoolExecutor.map(): таймаут не работает

import concurrent.futures
import time 

def process_one(i):
    try:                                                                             
        print("dealing with {}".format(i))                                           
        time.sleep(50)
        print("{} Done.".format(i))                                                  
    except Exception as e:                                                           
        print(e)

def process_many():
    with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: 
        executor.map(process_one,
                range(100),                                                          
                timeout=3)                                                           


if __name__ == '__main__':                                                           
    MAX_WORKERS = 10
    try:
        process_many()
    except Exception as e:                                                           
        print(e)      

Документы говорят:

__next__() итератор вызывает concurrent.futures.TimeoutError если __next__() и результат не доступен после timeout секунд от исходного вызова до Executor.map()

Но здесь сценарий не вызывал каких-либо исключений и продолжал ждать. Какие-либо предложения?

Ответ 1

Как указано в документах, ошибка таймаута будет повышаться только в том случае, если вы вызываете метод __next__() на карте. Чтобы вызвать этот метод, вы можете, например, преобразовать вывод в список:

from concurrent import futures
import threading
import time


def task(n):
    print("Launching task {}".format(n))
    time.sleep(n)
    print('{}: done with {}'.format(threading.current_thread().name, n))
    return n / 10


with futures.ThreadPoolExecutor(max_workers=5) as ex:
    results = ex.map(task, range(1, 6), timeout=3)
    print('main: starting')
    try:
        # without this conversion to a list, the timeout error is not raised
        real_results = list(results) 
    except futures._base.TimeoutError:
        print("TIMEOUT")

Выход:

Launching task 1
Launching task 2
Launching task 3
Launching task 4
Launching task 5
ThreadPoolExecutor-9_0: done with 1
ThreadPoolExecutor-9_1: done with 2
TIMEOUT
ThreadPoolExecutor-9_2: done with 3
ThreadPoolExecutor-9_3: done with 4
ThreadPoolExecutor-9_4: done with 5

Здесь n-я задача засыпает в течение n секунд, поэтому тайм-аут поднимается после завершения задания 2.


EDIT: Если вы хотите завершить задачи, которые не были выполнены, вы можете попробовать ответы в этом вопросе (они не используют ThreadPoolExecutor.map() хотя), или вы можете просто игнорировать возвращаемые значения из других задач и пусть они закончат:

from concurrent import futures
import threading
import time


def task(n):
    print("Launching task {}".format(n))
    time.sleep(n)
    print('{}: done with {}'.format(threading.current_thread().name, n))
    return n


with futures.ThreadPoolExecutor(max_workers=5) as ex:
    results = ex.map(task, range(1, 6), timeout=3)
    outputs = []
    try:
        for i in results:
            outputs.append(i)
    except futures._base.TimeoutError:
        print("TIMEOUT")
    print(outputs)

Выход:

Launching task 1
Launching task 2
Launching task 3
Launching task 4
Launching task 5
ThreadPoolExecutor-5_0: done with 1
ThreadPoolExecutor-5_1: done with 2
TIMEOUT
[1, 2]
ThreadPoolExecutor-5_2: done with 3
ThreadPoolExecutor-5_3: done with 4
ThreadPoolExecutor-5_4: done with 5

Ответ 2

Как мы видим в источнике (для python 3.7), карта возвращает функцию:

def map(self, fn, *iterables, timeout=None, chunksize=1):
    ...
    if timeout is not None:
        end_time = timeout + time.time()
    fs = [self.submit(fn, *args) for args in zip(*iterables)]
    # Yield must be hidden in closure so that the futures are submitted
    # before the first iterator value is required.
    def result_iterator():
        try:
            # reverse to keep finishing order
            fs.reverse()
            while fs:
                # Careful not to keep a reference to the popped future
                if timeout is None:
                    yield fs.pop().result()
                else:
                    yield fs.pop().result(end_time - time.time())
        finally:
            for future in fs:
                future.cancel()
    return result_iterator()

TimeoutError возникает из yield fs.pop().result(end_time - time.time()) но вы должны запросить результат для достижения этого вызова.

Обоснование в том, что вы не заботитесь о выполнении задач. Задачи отправляются и запускаются в фоновом режиме. То, о чем вы заботитесь, - это тайм-аут, когда вы запрашиваете результат - это обычный случай использования, когда вы отправляете задания, и вы запрашиваете у них результат в течение ограниченного времени, а не просто отправляете их и ожидаете, что они прекратятся в течение ограниченного времени.

Если последнее - это то, о чем вы говорили, вы можете использовать wait, как показано, например, в индивидуальных тайм-аутах для concurrent.futures