2

I want to run an async code as a background task from a sync function. My use case is that I have a huge application written in sync Python but I want to run some background tasks from it. An illustration from what I am doing is:

import asyncio
from time import sleep
import sys


async def task():
    for i in range(5):
        print(f"Background task iteration {i}")
        await asyncio.sleep(1)
    print('finished')


async def background_task():
    print("a")
    asyncio.create_task(task())
    print("b")


def main():
    print("Main program started python", sys.version)

    asyncio.run(background_task())
    for i in range(3):
        sleep(3)
        print(f"Main program iteration {i}")
        

if __name__ == "__main__":
    main()

The output:

Main program started python 3.11.6 (main, Oct 23 2023, 22:48:54) [GCC 11.4.0]
a
b
Background task iteration 0
Main program iteration 0
Main program iteration 1
Main program iteration 2

Why does the coroutine task never finish the loop it executes? The code never printed

Background task iteration 1
Background task iteration 2
Background task iteration 3
Background task iteration 4
finished

Why is only the first iteration executed?

3
  • Do you intend that "Main program iteration 1" happens after "finished" or that "Main program iteration" and "Background task iteration" happen in parallel? Commented Jan 4, 2024 at 10:13
  • I want to have Main program iteration 1 and Background task iteration to happen in parallel. (that's why I am using a background task). Commented Jan 4, 2024 at 10:23
  • You don't await anything in async def background_task. You just create a task task that prints Background task iteration 0 and then sits on await Commented Jan 4, 2024 at 10:25

2 Answers 2

3

Asyncio runs asynchronously which is not the same as in parallel, its not to be confused with threads which can be executed in parallel (on a high-level; take this with a grain of salt, see the comments). Asyncio is jumping from one async/await statement to the next executing code between two awaits in a sequential way.

In your code the task function is only scheduled via create_task. However the created task is not awaited it only executes until the first await through asyncio.create_task but then not further as there is no further await statement. For some hints see https://docs.python.org/3/library/asyncio-task.html#awaitables

starting the loop it reaches the first await.sleep and the sequence continues in your normal main loop. There the normal sleep does not communicate with asyncio and asyncio alone cannot execute further statements until the sequence reaches a new await, i.e. your background task is blocked.

Solution in Sequence

async def task():
    for i in range(5):
        print(f"Background task iteration {i}")
        await asyncio.sleep(0.1)
    print('finished')

async def background_task():
    print("a")
    scheduled_task = asyncio.create_task(task()) # here await was missing, 
    print("task scheduled")
    # do some stuff in between
    await scheduled_task # run your task function
    print("b")

def main():
    print("Main program started python", sys.version)

    asyncio.run(background_task())
    for i in range(3):
        sleep(0.5)
        print(f"Main program iteration {i}")

Output:

a
task sheduled
Background task iteration 0
Background task iteration 1
Background task iteration 2
Background task iteration 3
Background task iteration 4
finished
b
Main program iteration 0
Main program iteration 1
Main program iteration 2

Solution in Parallel: Combine with threading: to allow parallel execution of async code to your normal code.

import asyncio
from time import sleep
import sys
import threading

async def task():
    for i in range(5):
        print(f"Background task iteration {i}")
        await asyncio.sleep(1)
    print('finished')


async def background_task():
    print("a")
    await task() # Note if you do not need a task object this is sufficient.
    print("b")

def main():
    print("Main program started python", sys.version)

    t = threading.Thread(target=lambda: asyncio.run(background_task()))
    t.start()
    for i in range(3):
        sleep(3)
        print(f"Main program iteration {i}")

Output

a
Background task iteration 0
Background task iteration 1
Background task iteration 2
Main program iteration 0
Background task iteration 3
Background task iteration 4
finished
b
Main program iteration 1
Main program iteration 2
Sign up to request clarification or add additional context in comments.

4 Comments

The problem in the original code is not that the background task is "blocked" after the first await statement, but that the event loop is closed and all tasks are destroyed and asyncio.run returns.
BTW asyncio.gather is pointless with a single task. You can just write await task().
Another nitpick: it's true that threads can be executed in parallel, but in CPython they aren't because of the GIL. It's still just "concurrent".
Yes the parallel-threads need to be taken with a grain of salt, I kept it at a high-level. Good that you added it. Updated the answer.
1

The biggest problem, along side with the one already explained by some comments that you should use await asyncio.gather() instead of asyncio.create_task(), is that asyncio.run() runs in the main thread and thus pauses the execution. You can't run synchronous code at the same time as asynchronous code, unless you use a thread. (Duplicate: Python run non-blocking async function from sync function).

Another workaround would be to make main also asynchronous, as so:

import asyncio
import sys


async def task():
    for i in range(5):
        print(f"Background task iteration {i}")
        await asyncio.sleep(1)
    print('finished')


async def background_task():
    print("a")
    await asyncio.gather(task())
    print("b")


async def main():
    print("Main program started python", sys.version)

    for i in range(3):
        await asyncio.sleep(3)
        print(f"Main program iteration {i}")


async def runner():
    await asyncio.gather(main(), background_task())


if __name__ == "__main__":
    asyncio.run(runner())

This produces the result:

Main program started python 3.11.4 (tags/v3.11.4:d2340ef, Jun  7 2023, 05:45:37) [MSC v.1934 64 bit (AMD64)]
a
Background task iteration 0
Background task iteration 1
Background task iteration 2
Main program iteration 0
Background task iteration 3
Background task iteration 4
finished
b
Main program iteration 1
Main program iteration 2

Process finished with exit code 0

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.