You have:

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)

The main thread submits a task specifying worker function f1. Then the main thread exits the block and an implicit call to executor.shutdown() is made. Any tasks already submitted and have started to execute will complete but onceshutdown is called submitted tasks that have not yet started execution will be thrown away. In your code the call to shutdown occurs before worker function f1 has had a chance to submit the new task with f2 as the worker function and get its execution started. This can be demonstrated as follows:

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    import time
    time.sleep(.1)

We have delayed the call to shutdown by .1 seconds giving f1 a chance to get f2 started. But even this has a race condition: Is .1 seconds always enough time to allow f1 to submit the second task and for that task to start? We cannot depend on this method.

TL;DR

You can skip to the final section Solution if you wish and not read the following solutions for simpler cases.

Attempts

To remove that race condition we can use a multithreading.Event that gets set only after all tasks that we need to submit have started executing:

import concurrent.futures
from threading import Event

all_tasks_submitted = Event()

def f2():
    all_tasks_submitted.set()
    print("hello, f2")
    return 3


def f1():
    print("hello, f1")
    print(executor.submit(f2).result())


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    all_tasks_submitted.wait()

Prints:

hello, f1
hello, f2
3

So now let's look at your actual case. First, there is a slight bug: f2 takes only two arguments but f1 is trying to invoke it with 3 arguments.

This is far more complicated case in that we are ultimately trying to start 10 * 10 * 10 = 1000 f3 tasks. So we now need a counter to keep track of how many f3 have been started:

import concurrent.futures
from threading import Event, Lock

all_tasks_started = Event()
lock = Lock()


NUM_F3_TASKS = 1_000
total_f3_tasks_started = 0

def f3(arg1, arg2):
    global total_f3_tasks_started

    with lock:
        total_f3_tasks_started += 1
        n = total_f3_tasks_started

    if n == NUM_F3_TASKS:
        all_tasks_started.set()

    print(f"hello, f3, {arg1}, {arg2}, f3 tasks started = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    for i in range(10):
        executor.submit(f3, arg2, i)


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    for i in range(10):
        executor.submit(f2, arg2, arg3)


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    for i in range(10):
        executor.submit(f1, i, 1, 2)
    all_tasks_started.wait()

Prints:

hello, f1 0
hello, f1 1
hello, f1 2
hello, f1 3
hello, f1 4
hello, f1 5
hello, f1 6
hello, f1 7
hello, f1 8
hello, f1 9
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1

...

hello, f3, 2, 2, f3 tasks started = 993
hello, f3, 2, 4, f3 tasks started = 995
hello, f3, 2, 6, f3 tasks started = 997
hello, f3, 2, 8, f3 tasks started = 999
hello, f3, 2, 3, f3 tasks started = 994
hello, f3, 2, 7, f3 tasks started = 998
hello, f3, 2, 5, f3 tasks started = 996
hello, f3, 2, 9, f3 tasks started = 1000

But this means that you need to know in advance exactly how many f3 tasks need to be created. You might be tempted to solve the problem by having f1 not return until all tasks it has submitted complete and having f2 not return until all tasks it has submitted complete. You would thus be having a 10 f1 tasks, 100 f2 tasks and 1000 f3 tasks running concurrently for which you would need a thread pool of size 1110.

Solution

We use an explicit task queue and a task executor as follows:

import concurrent.futures
from queue import Queue
from threading import Lock

task_queue = Queue()
lock = Lock()

task_number = 0

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    print(f"hello, f3, {arg1}, {arg2}, task_number = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    for i in range(10):
        task_queue.put((f3, arg2, i))


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    for i in range(10):
        task_queue.put((f2, arg2, arg3))


def pool_executor():
    while True:
        task = task_queue.get()
        if task is None:
            # sentinel to terminate
            return

        fn, *args = task
        fn(*args)
        # Show this work has been completed:
        task_queue.task_done()


POOL_SIZE = 16

with concurrent.futures.ThreadPoolExecutor(POOL_SIZE) as executor:
    for _ in range(POOL_SIZE):
        executor.submit(pool_executor)

    for i in range(10):
        task_queue.put((f1, i, 1, 2))

    # Wait for all tasks to complete
    task_queue.join()
    # Now we need to terminate the running pool_executor tasks:
    # Add sentinels:
    for _ in range(POOL_SIZE):
        task_queue.put(None)

Prints:

hello, f1 0
hello, f1 1
hello, f1 3
hello, f1 5
hello, f1 7
hello, f1 9
hello, f1 2
hello, f2 1
hello, f2 1
hello, f2 1
hello, f1 4
hello, f1 6
hello, f1 8
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1

...

hello, f3, 2, 1, task_number = 992
hello, f3, 2, 2, task_number = 993
hello, f3, 2, 4, task_number = 995
hello, f3, 2, 6, task_number = 997
hello, f3, 2, 8, task_number = 999
hello, f3, 2, 3, task_number = 994
hello, f3, 2, 7, task_number = 998
hello, f3, 2, 5, task_number = 996
hello, f3, 2, 9, task_number = 1000

Perhaps you should consider creating your own thread pool with dameon threads, which will terminate when the main process terminates (you could still use the technique of adding sentinel values to signal these threads to terminate when we no longer require them in which case the threads need not be daemon threads).

from queue import Queue
from threading import Lock, Thread

...

def pool_executor():
    while True:
        fn, *args = task_queue.get()
        fn(*args)
        # Show this work has been completed:
        task_queue.task_done()


POOL_SIZE = 16

for _ in range(POOL_SIZE):
    Thread(target=pool_executor, daemon=True).start()

for i in range(10):
    task_queue.put((f1, i, 1, 2))

# Wait for all tasks to complete
task_queue.join()

A New Type of Multithreading Pool

We can abstract a multithreading pool that allows running tasks to continue to arbitrarily submit additional tasks and then be able to wait for all tasks to complete. That is, we wait until the task queue has quiesced, the condition where the task queue is empty and no new tasks will be added because there are no tasks currently running:

from queue import Queue
from threading import Thread

class ThreadPool:
    def __init__(self, pool_size):
        self._pool_size = pool_size
        self._task_queue = Queue()
        self._shutting_down = False
        for _ in range(self._pool_size):
            Thread(target=self._executor, daemon=True).start()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown()

    def _terminate_threads(self):
        """Tell threads to terminate."""
        # No new tasks in case this is an immediate shutdown:
        self._shutting_down = True

        for _ in range(self._pool_size):
            self._task_queue.put(None)
        self._task_queue.join()  # Wait for all threads to terminate


    def shutdown(self, wait=True):
        if wait:
            # Wait until the task queue quiesces (becomes empty).
            # Running tasks may be continuing to submit tasks to the queue but
            # the expectation is that at some point no more tasks will be added
            # and we wait for the queue to become empty:
            self._task_queue.join()
        self._terminate_threads()

    def submit(self, fn, *args):
        if self._shutting_down:
            return
        self._task_queue.put((fn, args))

    def _executor(self):
        while True:
            task = self._task_queue.get()
            if task is None:  # sentinel
                self._task_queue.task_done()
                return
            fn, args = task
            try:
                fn(*args)
            except Exception as e:
                print(e)
            # Show this work has been completed:
            self._task_queue.task_done()

###############################################

from threading import Lock

lock = Lock()

task_number = 0

results = []

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    #print(f"hello, f3, {arg1}, {arg2}, task_number = {n}")
    results.append(f"hello, f3, {arg1}, {arg2}, task_number = {n}")


def f2(arg1, arg2):
    for i in range(10):
        pool.submit(f3, arg2, i)

def f1(arg1, arg2, arg3):
    for i in range(10):
        pool.submit(f2, arg2, arg3)


with ThreadPool(16) as pool:
    for i in range(10):
        pool.submit(f1, i, 1, 2)

for result in results:
    print(result)

Another Way That Uses Standard concurrent.futures Methods

As you have observed, in the above solution an f1 task will complete before the f2 tasks it has submitted has completed and f2 tasks will terminate before f3 tasks have terminated. The problem with your original code was due to a shutdown being implicitly called before all 1000 f3 tasks were submitted. We can prevent this premature shutdown from occuring by having each worker function return a list of Future instance whose results we await:

from concurrent.futures import ThreadPoolExecutor, Future
from threading import Lock

task_number = 0

lock = Lock()

futures = []

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    print(f"hello, f3, {arg1}, {arg2}, f3 tasks started = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    futures.extend(
        executor.submit(f3, arg2, i)
        for i in range(10)
    )


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    futures.extend(
        executor.submit(f2, arg2, arg3)
        for i in range(10)
    )


with ThreadPoolExecutor(16) as executor:
    futures.extend(
        executor.submit(f1, i, 1, 2)
        for i in range(10)
    )

    cnt = 0
    for future in futures:
        future.result()
        cnt += 1
    print(cnt, 'tasks completed.')

Prints:

...
hello, f3, 2, 4, f3 tasks started = 995
hello, f3, 2, 6, f3 tasks started = 997
hello, f3, 2, 8, f3 tasks started = 999
hello, f3, 2, 3, f3 tasks started = 994
hello, f3, 2, 7, f3 tasks started = 998
hello, f3, 2, 5, f3 tasks started = 996
hello, f3, 2, 9, f3 tasks started = 1000
1110 tasks completed.
Answer from Booboo on Stack Overflow
🌐
Python
docs.python.org β€Ί 3 β€Ί library β€Ί concurrent.futures.html
concurrent.futures β€” Launching parallel tasks β€” Python 3.14 ...
January 30, 2026 - A ThreadPoolExecutor subclass that executes calls asynchronously using a pool of at most max_workers threads. Each thread runs tasks in its own interpreter. The worker interpreters are isolated from each other, which means each has its own runtime state and that they can’t share any mutable objects or other data.
🌐
GeeksforGeeks
geeksforgeeks.org β€Ί python β€Ί how-to-use-threadpoolexecutor-in-python3
How to use ThreadPoolExecutor in Python3 ? - GeeksforGeeks
July 23, 2025 - The below code demonstrates the use of ThreadPoolExecutor, notice unlike with the threading module we do not have to explicitly call using a loop, keeping a track of thread using a list or wait for threads using join for synchronization, or releasing the resources after the threads are finished everything is taken under the hood by the constructor itself making the code compact and bug-free.
🌐
Python Engineer
python-engineer.com β€Ί posts β€Ί threadpoolexecutor
How to use ThreadPoolExecutor in Python - Python Engineer
May 2, 2022 - from concurrent.futures import ThreadPoolExecutor urls = ["python-engineer.com", "twitter.com", "youtube.com"] def scrape_site(url): res = f'{url} was scraped!' return res pool = ThreadPoolExecutor(max_workers=8) results = pool.map(scrape_site, urls) # does not block for res in results: print(res) # print results as they become available pool.shutdown()
🌐
TutorialEdge
tutorialedge.net β€Ί python β€Ί concurrency β€Ί python-threadpoolexecutor-tutorial
Python ThreadPoolExecutor Tutorial | TutorialEdge.net
When we execute the above program you should see that it prints out that we are starting out ThreadPoolExecutor before going on to execute the three distinct tasks we submit to it and then finally printing out that all tasks are complete. $ python3.6 01_threadPoolExe.py Starting ThreadPoolExecutor Processing 2 Processing 3 Processing 4 All tasks complete
🌐
Medium
medium.com β€Ί @anupchakole β€Ί understanding-threadpoolexecutor-2eed095d21aa
Understanding ThreadPoolExecutor. ThreadPoolExecutor is a Python class… | by Anup Chakole | Medium
August 28, 2024 - Experiment with Concurrency Levels: Test different max_workers values in ThreadPoolExecutor to find the optimal balance between concurrency and resource usage.
🌐
Python Tutorial
pythontutorial.net β€Ί home β€Ί python concurrency β€Ί python threadpoolexecutor
Python ThreadPoolExecutor By Practical Examples
July 15, 2022 - Starting the task 1... Starting the task 2... Done with task 1 Done with task 2 It took 1.0177214 second(s) to finish.Code language: Python (python) The output shows that the program took about 1 second to finish.
🌐
DigitalOcean
digitalocean.com β€Ί community β€Ί tutorials β€Ί how-to-use-threadpoolexecutor-in-python-3
How To Use ThreadPoolExecutor in Python 3 | DigitalOcean
June 23, 2020 - Warning: In general, it is not safe to share Python objects or state between threads without taking special care to avoid concurrency bugs. When defining a function to execute in a thread, it is best to define a function that performs a single job and does not share or publish state to other threads. get_wiki_page_existence is an example of such a function. Now that we have a function well suited to invocation with threads, we can use ThreadPoolExecutor to perform multiple invocations of that function expediently.
Find elsewhere
Top answer
1 of 3
2

You have:

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)

The main thread submits a task specifying worker function f1. Then the main thread exits the block and an implicit call to executor.shutdown() is made. Any tasks already submitted and have started to execute will complete but onceshutdown is called submitted tasks that have not yet started execution will be thrown away. In your code the call to shutdown occurs before worker function f1 has had a chance to submit the new task with f2 as the worker function and get its execution started. This can be demonstrated as follows:

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    import time
    time.sleep(.1)

We have delayed the call to shutdown by .1 seconds giving f1 a chance to get f2 started. But even this has a race condition: Is .1 seconds always enough time to allow f1 to submit the second task and for that task to start? We cannot depend on this method.

TL;DR

You can skip to the final section Solution if you wish and not read the following solutions for simpler cases.

Attempts

To remove that race condition we can use a multithreading.Event that gets set only after all tasks that we need to submit have started executing:

import concurrent.futures
from threading import Event

all_tasks_submitted = Event()

def f2():
    all_tasks_submitted.set()
    print("hello, f2")
    return 3


def f1():
    print("hello, f1")
    print(executor.submit(f2).result())


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    all_tasks_submitted.wait()

Prints:

hello, f1
hello, f2
3

So now let's look at your actual case. First, there is a slight bug: f2 takes only two arguments but f1 is trying to invoke it with 3 arguments.

This is far more complicated case in that we are ultimately trying to start 10 * 10 * 10 = 1000 f3 tasks. So we now need a counter to keep track of how many f3 have been started:

import concurrent.futures
from threading import Event, Lock

all_tasks_started = Event()
lock = Lock()


NUM_F3_TASKS = 1_000
total_f3_tasks_started = 0

def f3(arg1, arg2):
    global total_f3_tasks_started

    with lock:
        total_f3_tasks_started += 1
        n = total_f3_tasks_started

    if n == NUM_F3_TASKS:
        all_tasks_started.set()

    print(f"hello, f3, {arg1}, {arg2}, f3 tasks started = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    for i in range(10):
        executor.submit(f3, arg2, i)


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    for i in range(10):
        executor.submit(f2, arg2, arg3)


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    for i in range(10):
        executor.submit(f1, i, 1, 2)
    all_tasks_started.wait()

Prints:

hello, f1 0
hello, f1 1
hello, f1 2
hello, f1 3
hello, f1 4
hello, f1 5
hello, f1 6
hello, f1 7
hello, f1 8
hello, f1 9
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1

...

hello, f3, 2, 2, f3 tasks started = 993
hello, f3, 2, 4, f3 tasks started = 995
hello, f3, 2, 6, f3 tasks started = 997
hello, f3, 2, 8, f3 tasks started = 999
hello, f3, 2, 3, f3 tasks started = 994
hello, f3, 2, 7, f3 tasks started = 998
hello, f3, 2, 5, f3 tasks started = 996
hello, f3, 2, 9, f3 tasks started = 1000

But this means that you need to know in advance exactly how many f3 tasks need to be created. You might be tempted to solve the problem by having f1 not return until all tasks it has submitted complete and having f2 not return until all tasks it has submitted complete. You would thus be having a 10 f1 tasks, 100 f2 tasks and 1000 f3 tasks running concurrently for which you would need a thread pool of size 1110.

Solution

We use an explicit task queue and a task executor as follows:

import concurrent.futures
from queue import Queue
from threading import Lock

task_queue = Queue()
lock = Lock()

task_number = 0

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    print(f"hello, f3, {arg1}, {arg2}, task_number = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    for i in range(10):
        task_queue.put((f3, arg2, i))


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    for i in range(10):
        task_queue.put((f2, arg2, arg3))


def pool_executor():
    while True:
        task = task_queue.get()
        if task is None:
            # sentinel to terminate
            return

        fn, *args = task
        fn(*args)
        # Show this work has been completed:
        task_queue.task_done()


POOL_SIZE = 16

with concurrent.futures.ThreadPoolExecutor(POOL_SIZE) as executor:
    for _ in range(POOL_SIZE):
        executor.submit(pool_executor)

    for i in range(10):
        task_queue.put((f1, i, 1, 2))

    # Wait for all tasks to complete
    task_queue.join()
    # Now we need to terminate the running pool_executor tasks:
    # Add sentinels:
    for _ in range(POOL_SIZE):
        task_queue.put(None)

Prints:

hello, f1 0
hello, f1 1
hello, f1 3
hello, f1 5
hello, f1 7
hello, f1 9
hello, f1 2
hello, f2 1
hello, f2 1
hello, f2 1
hello, f1 4
hello, f1 6
hello, f1 8
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1
hello, f2 1

...

hello, f3, 2, 1, task_number = 992
hello, f3, 2, 2, task_number = 993
hello, f3, 2, 4, task_number = 995
hello, f3, 2, 6, task_number = 997
hello, f3, 2, 8, task_number = 999
hello, f3, 2, 3, task_number = 994
hello, f3, 2, 7, task_number = 998
hello, f3, 2, 5, task_number = 996
hello, f3, 2, 9, task_number = 1000

Perhaps you should consider creating your own thread pool with dameon threads, which will terminate when the main process terminates (you could still use the technique of adding sentinel values to signal these threads to terminate when we no longer require them in which case the threads need not be daemon threads).

from queue import Queue
from threading import Lock, Thread

...

def pool_executor():
    while True:
        fn, *args = task_queue.get()
        fn(*args)
        # Show this work has been completed:
        task_queue.task_done()


POOL_SIZE = 16

for _ in range(POOL_SIZE):
    Thread(target=pool_executor, daemon=True).start()

for i in range(10):
    task_queue.put((f1, i, 1, 2))

# Wait for all tasks to complete
task_queue.join()

A New Type of Multithreading Pool

We can abstract a multithreading pool that allows running tasks to continue to arbitrarily submit additional tasks and then be able to wait for all tasks to complete. That is, we wait until the task queue has quiesced, the condition where the task queue is empty and no new tasks will be added because there are no tasks currently running:

from queue import Queue
from threading import Thread

class ThreadPool:
    def __init__(self, pool_size):
        self._pool_size = pool_size
        self._task_queue = Queue()
        self._shutting_down = False
        for _ in range(self._pool_size):
            Thread(target=self._executor, daemon=True).start()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown()

    def _terminate_threads(self):
        """Tell threads to terminate."""
        # No new tasks in case this is an immediate shutdown:
        self._shutting_down = True

        for _ in range(self._pool_size):
            self._task_queue.put(None)
        self._task_queue.join()  # Wait for all threads to terminate


    def shutdown(self, wait=True):
        if wait:
            # Wait until the task queue quiesces (becomes empty).
            # Running tasks may be continuing to submit tasks to the queue but
            # the expectation is that at some point no more tasks will be added
            # and we wait for the queue to become empty:
            self._task_queue.join()
        self._terminate_threads()

    def submit(self, fn, *args):
        if self._shutting_down:
            return
        self._task_queue.put((fn, args))

    def _executor(self):
        while True:
            task = self._task_queue.get()
            if task is None:  # sentinel
                self._task_queue.task_done()
                return
            fn, args = task
            try:
                fn(*args)
            except Exception as e:
                print(e)
            # Show this work has been completed:
            self._task_queue.task_done()

###############################################

from threading import Lock

lock = Lock()

task_number = 0

results = []

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    #print(f"hello, f3, {arg1}, {arg2}, task_number = {n}")
    results.append(f"hello, f3, {arg1}, {arg2}, task_number = {n}")


def f2(arg1, arg2):
    for i in range(10):
        pool.submit(f3, arg2, i)

def f1(arg1, arg2, arg3):
    for i in range(10):
        pool.submit(f2, arg2, arg3)


with ThreadPool(16) as pool:
    for i in range(10):
        pool.submit(f1, i, 1, 2)

for result in results:
    print(result)

Another Way That Uses Standard concurrent.futures Methods

As you have observed, in the above solution an f1 task will complete before the f2 tasks it has submitted has completed and f2 tasks will terminate before f3 tasks have terminated. The problem with your original code was due to a shutdown being implicitly called before all 1000 f3 tasks were submitted. We can prevent this premature shutdown from occuring by having each worker function return a list of Future instance whose results we await:

from concurrent.futures import ThreadPoolExecutor, Future
from threading import Lock

task_number = 0

lock = Lock()

futures = []

def f3(arg1, arg2):
    global task_number

    with lock:
        task_number += 1
        n = task_number

    print(f"hello, f3, {arg1}, {arg2}, f3 tasks started = {n}")


def f2(arg1, arg2):
    print(f"hello, f2 {arg1}")
    futures.extend(
        executor.submit(f3, arg2, i)
        for i in range(10)
    )


def f1(arg1, arg2, arg3):
    print(f"hello, f1 {arg1}")
    futures.extend(
        executor.submit(f2, arg2, arg3)
        for i in range(10)
    )


with ThreadPoolExecutor(16) as executor:
    futures.extend(
        executor.submit(f1, i, 1, 2)
        for i in range(10)
    )

    cnt = 0
    for future in futures:
        future.result()
        cnt += 1
    print(cnt, 'tasks completed.')

Prints:

...
hello, f3, 2, 4, f3 tasks started = 995
hello, f3, 2, 6, f3 tasks started = 997
hello, f3, 2, 8, f3 tasks started = 999
hello, f3, 2, 3, f3 tasks started = 994
hello, f3, 2, 7, f3 tasks started = 998
hello, f3, 2, 5, f3 tasks started = 996
hello, f3, 2, 9, f3 tasks started = 1000
1110 tasks completed.
2 of 3
0

Looks like, in your first example, maybe the program terminates before the "f2" thread gets its chance to print.

Adding a time.sleep call to the very end of your first example allows it to print both "hello" messages when I run it.*

import concurrent.futures
import time

def f2():
    print("hello, f2")


def f1():
    print("hello, f1")
    executor.submit(f2)


with concurrent.futures.ThreadPoolExecutor(16) as executor:
    executor.submit(f1)
    time.sleep(3)

I have not ever used concurrent.futures. Are its worker threads daemons?†


Update:

I wonder why main thread cannot find the f2 future...

Not sure what you're asking, but try this:

import concurrent.futures
import queue

q = queue.Queue(2)

def f2():
    print("hello, f2")

def f1():
    print("hello, f1")
    future_2 = executor.submit(f2)
    q.put(future_2)

with concurrent.futures.ThreadPoolExecutor(16) as executor:
    future_1 = executor.submit(f1)
    future_2 = q.get()
    future_1.result()
    future_2.result()

* Python 3.12.4 running on macOS Sonoma 14.4.1.

† I can't find "daemon" or "demon" anywhere in The documentation, and I can't find out by using Threading.current_thread().daemon because the current_thread function only works in threads that were created directly by the Threading module. current_thread returns a bogus object when called by a concurrent.futures worker thread.

🌐
Medium
medium.com β€Ί @superfastpython β€Ί python-threadpoolexecutor-7-day-crash-course-78d4846d5acc
Python ThreadPoolExecutor: 7-Day Crash Course | by Super Fast Python | Medium
December 3, 2023 - Python ThreadPoolExecutor: 7-Day Crash Course The Python ThreadPoolExecutor allows us to create and manage thread pools in Python. Although the ThreadPoolExecutor has been available since Python 3.2 …
🌐
Medium
blog.wahab2.com β€Ί python-threadpoolexecutor-use-cases-for-parallel-processing-3d5c90fd5634
Python ThreadPoolExecutor: Use Cases for Parallel Processing | by Abdul Rafee Wahab | Medium
August 23, 2023 - ThreadPoolExecutor is a built-in Python module that allows us to create a pool of threads to execute tasks in parallel. In this segment, we will explore the ThreadPoolExecutor module in detail, including its use cases, functionality, and examples.
🌐
Tiew Kee Hui's Blog
tiewkh.github.io β€Ί blog β€Ί python-thread-pool-executor
Shutting Down Python's ThreadPoolExecutor
July 10, 2021 - With a bit of knowledge of object-oriented programming, we can! Simply inherit the ThreadPoolExecutor class and add a new method (in case we still want the original shutdown()) with Python 3.9’s version of shutdown().
🌐
pytz
pythonhosted.org β€Ί futures
concurrent.futures β€” Asynchronous computation β€” futures 2.1.3 documentation
Regardless of the value of wait, the entire Python program will not exit until all pending futures are done executing. You can avoid having to call this method explicitly if you use the with statement, which will shutdown the Executor (waiting as if Executor.shutdown were called with wait set to True): import shutil with ThreadPoolExecutor(max_workers=4) as e: e.submit(shutil.copy, 'src1.txt', 'dest1.txt') e.submit(shutil.copy, 'src2.txt', 'dest2.txt') e.submit(shutil.copy, 'src3.txt', 'dest3.txt') e.submit(shutil.copy, 'src3.txt', 'dest4.txt')
🌐
Reddit
reddit.com β€Ί r/python β€Ί threadpoolexecutor in python: the complete guide
r/Python on Reddit: ThreadPoolExecutor in Python: The Complete Guide
February 12, 2022 - Stay up to date with the latest news, packages, and meta information relating to the Python programming language. --- If you have questions or are new to Python use r/LearnPython ... Archived post. New comments cannot be posted and votes cannot be cast. Share ... ThreadPoolExecutor: I'm gonna fuck this up three times before I get it right for the 50th time.
🌐
DEV Community
dev.to β€Ί blamsa0mine β€Ί -boost-your-python-tasks-with-threadpoolexecutor-4dbf
# Boost Your Python Tasks with `ThreadPoolExecutor` - DEV Community
December 7, 2024 - Here's a real-world example: fetching multiple URLs in parallel. import requests from concurrent.futures import ThreadPoolExecutor # Function to fetch a URL def fetch_url(url): try: response = requests.get(url) return f"URL: {url}, Status: ...
🌐
ZetCode
zetcode.com β€Ί python β€Ί threadpoolexecutor
Python ThreadPoolExecutor - concurrency in Python with ThreadPoolExecutor
This example uses ThreadPoolExecutor to download multiple images from URLs concurrently, leveraging the requests library for HTTP requests. ... #!/usr/bin/python import requests from concurrent.futures import ThreadPoolExecutor, as_completed from time import perf_counter def download_image(url): ...
🌐
Super Fast Python
superfastpython.com β€Ί home β€Ί tutorials β€Ί threadpoolexecutor example in python
ThreadPoolExecutor Example in Python - Super Fast Python
September 12, 2022 - We will use this problem as the basis to explore the different patterns with the ThreadPoolExecutor for downloading files concurrently. This example is divided into three parts; they are: ... First, let’s develop a serial (non-concurrent) version of the program. Run loops using all CPUs, download your FREE book to learn how. Consider the situation where we might want to have a local copy of some of the Python API documentation on concurrency for later review.
🌐
TutorialsPoint
tutorialspoint.com β€Ί concurrency_in_python β€Ί concurrency_in_python_pool_of_threads.htm
Concurrency in Python - Pool of Threads
with ThreadPoolExecutor(max_workers = 5) as executor Β· The following example is borrowed from the Python docs. In this example, first of all the concurrent.futures module has to be imported. Then a function named load_url() is created which will load the requested url.
🌐
Python Module of the Week
pymotw.com β€Ί 3 β€Ί concurrent.futures
concurrent.futures β€” Manage Pools of Concurrent Tasks
from concurrent import futures import threading import time def task(n): print('{}: sleeping {}'.format( threading.current_thread().name, n) ) time.sleep(n / 10) print('{}: done with {}'.format( threading.current_thread().name, n) ) return n / 10 ex = futures.ThreadPoolExecutor(max_workers=2) print('main: starting') f = ex.submit(task, 5) print('main: future: {}'.format(f)) print('main: waiting for results') result = f.result() print('main: result: {}'.format(result)) print('main: future after result: {}'.format(f)) The status of the future changes after the tasks is completed and the result i
🌐
Python.org
discuss.python.org β€Ί python help
Concurrent usage of concurrent.futures.ThreadPoolExecutor - Python Help - Discussions on Python.org
December 8, 2023 - Is concurrent.futures.ThreadPoolExecutor safe to use concurrently? I seem to run into a deadlock when I have a concurrent.futures.ThreadPoolExecutor.map call when the function being mapped submits additional tasks to the same pool and calls result() on them Β· You only have a finite pool of ...