<div dir="ltr"><div>Hi,</div><div><br></div><div>I've been using asyncio a lot lately and have encountered this problem several times. Imagine you want to do a lot of queries against a database, spawning 10000 tasks in parallel will probably cause a lot of them to fail. What you need in a task pool of sorts, to limit concurrency and do only 20 requests in parallel.<br></div><div><br></div><div>If we were doing this synchronously, we wouldn't spawn 10000 threads using 10000 connections, we would use a thread pool with a limited number of threads and submit the jobs into its queue.</div><div><br></div><div>To me, tasks are (somewhat) logically analogous to threads. The solution that first comes to mind is to create an AsyncioTaskExecutor with a submit(coro, *args, **kwargs) method. Put a reference to the coroutine and its arguments into an asyncio queue. Spawn n tasks pulling from this queue and awaiting the coroutines.</div><div><br></div><div>It'd probably be useful to have this in the stdlib at some point.<br></div><div><div class="gmail_quote"><br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">
Date: Wed, 13 Jun 2018 22:45:22 +0200<br>
From: Michel Desmoulin <<a href="mailto:desmoulinmichel@gmail.com" target="_blank">desmoulinmichel@gmail.com</a>><br>
To: <a href="mailto:python-dev@python.org" target="_blank">python-dev@python.org</a><br>
Subject: [Python-Dev] A more flexible task creation<br>
Message-ID: <<a href="mailto:bca6b319-c436-c8c2-bb0e-6707f0495c49@gmail.com" target="_blank">bca6b319-c436-c8c2-bb0e-6707f0495c49@gmail.com</a>><br>
Content-Type: text/plain; charset=utf-8<br>
<br>
I was working on a concurrency limiting code for asyncio, so the user<br>
may submit as many tasks as one wants, but only a max number of tasks<br>
will be submitted to the event loop at the same time.<br>
<br>
However, I wanted that passing an awaitable would always return a task,<br>
no matter if the task was currently scheduled or not. The goal is that<br>
you could add done callbacks to it, decide to force schedule it, etc<br>
<br>
I dug in the asyncio.Task code, and encountered:<br>
<br>
    def __init__(self, coro, *, loop=None):<br>
        ...<br>
        self._loop.call_soon(self._step)<br>
        self.__class__._all_tasks.add(self)<br>
<br>
I was surprised to see that instantiating a Task class has any side<br>
effect at all, let alone 2, and one of them being to be immediately<br>
scheduled for execution.<br>
<br>
I couldn't find a clean way to do what I wanted: either you<br>
loop.create_task() and you get a task but it runs, or you don't run<br>
anything, but you don't get a nice task object to hold on to.<br>
<br>
I tried several alternatives, like returning a future, and binding the<br>
future awaiting to the submission of a task, but that was complicated<br>
code that duplicated a lot of things.<br>
<br>
I tried creating a custom task, but it was even harder, setting a custom<br>
event policy, to provide a custom event loop with my own create_task()<br>
accepting parameters. That's a lot to do just to provide a parameter to<br>
Task, especially if you already use a custom event loop (e.g: uvloop). I<br>
was expecting to have to create a task factory only, but task factories<br>
can't get any additional parameters from create_task()).<br>
<br>
Additionally I can't use ensure_future(), as it doesn't allow to pass<br>
any parameter to the underlying Task, so if I want to accept any<br>
awaitable in my signature, I need to provide my own custom ensure_future().<br>
<br>
All those implementations access a lot of _private_api, and do other<br>
shady things that linters hate; plus they are fragile at best. What's<br>
more, Task being rewritten in C prevents things like setting self._coro,<br>
so we can only inherit from the pure Python slow version.<br>
<br>
In the end, I can't even await the lazy task, because it blocks the<br>
entire program.<br>
<br>
Hence I have 2 distinct, but independent albeit related, proposals:<br>
<br>
- Allow Task to be created but not scheduled for execution, and add a<br>
parameter to ensure_future() and create_task() to control this. Awaiting<br>
such a task would just do like asyncio.sleep(O) until it is scheduled<br>
for execution.<br>
<br>
- Add an parameter to ensure_future() and create_task() named "kwargs"<br>
that accept a mapping and will be passed as **kwargs to the underlying<br>
created Task.<br>
<br>
I insist on the fact that the 2 proposals are independent, so please<br>
don't reject both if you don't like one or the other. Passing a<br>
parameter to the underlying custom Task is still of value even without<br>
the unscheduled instantiation, and vice versa.<br>
<br>
Also, if somebody has any idea on how to make a LazyTask that we can<br>
await on without blocking everything, I'll take it.<br>
<br>
</blockquote></div></div></div>