4

I have an async function that listens on a specific port. I want to run the function on a few ports at a time and when the user wants to stop listening on a specific port, stop the function listening on that port.

Previously I was using the asyncio library for this task and I tackled this problem by creating tasks with a unique id as their name.

asyncio.create_task(Func(), name=UNIQUE_ID)

Since trio uses nurseries to spawn tasks, I can see the running tasks by using nursery.child_tasks but the tasks don't have a way to name them and even a way to cancel a task on demand

TL;DR

Since trio doesn't has a cancel() function that cancels a specific task, how can I manually cancel a task.

1 Answers1

3

Easy. You create a cancel scope, return that from the task, and cancel this scope when required:

async def my_task(task_status=trio.TASK_STATUS_IGNORED):
    with trio.CancelScope() as scope:
        task_status.started(scope)
        pass  # do whatever

async def main():
    async with trio.open_nursery() as n:
        scope = await n.start(my_task)
        pass  # do whatever
        scope.cancel()  # cancels my_task()

The magic part is await n.start(task), which waits until the task calls task_status.started(x) and returns the value of x (or None if you leave that empty).

Matthias Urlichs
  • 1,789
  • 14
  • 26
  • Can you please explain what this code is doing? I tried reading the documentation but I still don't know how this works. Also is it possible to use the nursery.start_soon() function and still have the cancel() function to cancel the child tasks? –  Mar 19 '20 at 10:13
  • 1
    What's to explain? your task runs in a scope, you can cancel scopes, thus `task_status.started()` passes that back to `main` via the result of `await nursery.start` so that `main` can do the cancelling if it needs to. – Matthias Urlichs Mar 20 '20 at 11:49
  • You need to wait until the task has proceeded to the point where it creates the scope and passes a handle back. You can certainly do all of that manually, with a shared object and a `trio.Event` or whatever, but why would you want to when Trio already gives you a built-in one-liner way to do it? – Matthias Urlichs Mar 20 '20 at 11:53
  • I also don't understand it. It looks like the task being started just opens it up to be cancelled at some point but not at will from an external function or event. Like if I'm using Trio to run a background task that never returns and runs forever, how do I stop it when some value becomes True, or something? I've been using a Trio task alongside asyncio and when trying to stop the program, asyncio raises a runtime error saying that the loop is already closed. So I can only `sys.exit()` or `os._exit(0)` or else the trio portion continues forever. I don't think my method is efficient. – Break Dec 02 '20 at 21:01
  • What do you mean, how do you stop it? at some point your task is going to call `await trio.sleep()` or basically any other async Trio function. At that point Trio checks if somebody has called `cancel` on any of the cancel scope it's running in, if so it raises a `trio.Cancelled` exception. No, you cannot use trio along with asyncio that way. They're different. The Trio docs clearly say this. There's `trio_asyncio` if you really need to do that. – Matthias Urlichs Dec 04 '20 at 06:53
  • The thing I missed in this code initially is that it is starting the task with `result = await nursery.start(my_task)`, rather than the more common way of using `nursery.start_soon(my_task)` (with no `await`). As the [docs say](https://trio.readthedocs.io/en/stable/reference-core.html?highlight=multierror#trio.Nursery.start), that has two effects: (1) the `task` starts running immediately, blocking the parent task, until the child task calls `task_status.started()`; (2) the parameter passed to `started()` is passed up to the parent task as the result of `start_soon()`. – Jim Oldfield Jan 14 '21 at 13:31