I’m -1 on this proposal for two reasons:
- We like the fine precision over when we create, start, and join threads, which the current
threading library provides. This proposal merges them together, which we don’t always want, and therefore only serves to occasionally save 3 lines.
- If
threading.run is meant to parallel asyncio.run I don’t think they’re really comparable.
1.
Often one wants to get all the threads ready before starting any of them. Instead of
for _ in range(N_THREADS):
t = threading.Thread(target = do_stuff)
t.start()
threads.append(t)
which is like what you’re suggesting with threading.run(), one wants
for _ in range(N_THREADS):
t = threading.Thread(target = do_stuff)
threads.append(t)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
especially true for the thread.join() loop being separated from the thread.start() loop.
2.
If this proposal is trying to parallel asyncio.run, the reason (to me) that asyncio.run exists is because we need to create a synchronous runner which can handle asyncio coroutines, that can wait for one to complete without having to use the await keyword. ie we need
import asyncio
async def amain() -> None:
await asyncio.sleep(1)
asyncio.run(amain())
because we can’t do
import asyncio
async def amain() -> None:
await asyncio.sleep(1)
await amain() # SyntaxError: 'await' outside function
asyncio.run is nothing special with respect to “does it import special _asyncio C code” (no). It’s just a very convenient wrapper around creating an asyncio event loop. But it does functionally serve a special purpose as the ‘head’ of one’s asyncio code. It’s the same as if one were to use yield from to create incremental functions. One wants a wrapper around the top layer:
PEP 380 `yield from` inspired PEP 3156 `asyncio`
from typing import Generator
from contextlib import closing
class IncrementalDecoder:
def __init__(self) -> None:
self._bytes_buffer: bytes = b""
self._results: list[int] = []
self._generator = self._parse_forever()
self._wakeup()
def decode(self, input: bytes, final: bool = False) -> tuple[int]:
self._bytes_buffer += input
self._wakeup()
if final:
self._shutdown()
ret = tuple(self._results)
self._results.clear()
return ret
def close(self) -> None:
self.decode(b"", final = True)
def _wakeup(self) -> None:
self._generator.send(None)
def _shutdown(self) -> None:
self._generator.close()
def _await_should_parse_again(self) -> Generator[None, None, None]:
while not self._bytes_buffer:
yield
def _await_bytes(self, n_bytes: int) -> Generator[None, None, bytes]:
while len(self._bytes_buffer) < n_bytes:
yield
ret = self._bytes_buffer[:n_bytes]
self._bytes_buffer = self._bytes_buffer[n_bytes:]
return ret
def _await_byte(self) -> Generator[None, None, int]:
return (yield from self._await_bytes(1))[0]
def _parse_single(self) -> Generator[None, None, int]:
b1 = yield from self._await_byte()
b2 = yield from self._await_byte()
b3 = yield from self._await_byte()
b4 = yield from self._await_byte()
return (b1 << 24) + (b2 << 16) + (b3 << 8) + (b4 << 0)
def _parse_forever(self) -> Generator[None, None, None]:
while True:
try:
yield from self._await_should_parse_again()
# Park the generator in this 'parking lot'
# Safe to close() here
except GeneratorExit:
return
try:
x = yield from self._parse_single()
except GeneratorExit as e:
raise EOFError("Partially decoded unit didn't finish") from e
else:
self._results.append(x)
decoder = IncrementalDecoder()
with closing(decoder):
print(decoder.decode(b"H")) # ()
print(decoder.decode(b"ello ")) # (1214606444,)
print(decoder.decode(b"World!")) # (1864390511, 1919706145)
Threads however just come across as a normal resource like a file descriptor that we can close() etc, or a process that we can waitpid() etc. I don’t think they need a higher-level runner-wrapper like asyncio.run() creates for us.