Создание пула рабочих не передаёт вызывающему загруженные страницы. Сохраняйте и потребляйте значения, которые возвращает отправленная работа:

from concurrent.futures import ThreadPoolExecutor
import urllib3

manager = urllib3.ProxyManager(
    'http://proxy.example:8080',
    maxsize=4,
    block=True,
)

def fetch(url):
    response = manager.request(
        'GET', url, timeout=urllib3.Timeout(connect=5, read=15),
    )
    return url, response.status, response.data

try:
    with ThreadPoolExecutor(max_workers=4) as workers:
        results = list(workers.map(fetch, urls))
finally:
    manager.clear()

Этот фрагмент предполагает ограниченный список URL и обычные размеры ответов. Для разрешённых проверок публичных URL без прокси-аутентификации кандидат из списка HTTP-прокси может заменить примерную HTTP-точку входа менеджера. Каждый результат содержит свой URL, чтобы вызывающий мог связать статус и тело с правильным запросом. map возвращает результаты в порядке ввода; исключение в рабочем потоке возникает при получении этого результата.

Настроенный ProxyManager владеет пулом соединений для запросов. С используемым здесь режимом предварительно загруженного ответа по умолчанию urllib3 читает тело и возвращает соединение в свой пул. Потоковой версии потребовалось бы явное потребление и освобождение соединения.

maxsize обычно управляет сохраняемыми переиспользуемыми соединениями, а не жёстким ограничением на каждый открытый сокет. block=True меняет соответствующее поведение пула, чтобы вызывающие ждали свободное соединение, а не открывали дополнительные сверх лимита этого пула.

Число рабочих ограничивает одновременные вызовы в этом примере. При множестве назначений также важны схема пула и маршрут для туннелей HTTPS; настройку одного пула не следует описывать как единую глобальную квоту сокетов для каждого хоста.

Похожие статьи