Re: What does stats = await asyncio.to_thread(os.stat, url) do? (Was async I/O via threads is extremly slow)

"Peter J. Holzer" <[email protected]>
Newsgroups gmane.comp.python.general
Message-ID <[email protected]>
On 2025-06-24 00:32:26 +0200, Mild Shock wrote:
> So what does:
> 
> stats = await asyncio.to_thread(os.stat, url)
> 
> Whell it calls in a sparate new secondary thread:
> 
> os.stat(url)
> 
> It happends that url is only a file path, and
> the file path points to an existing file. So the
> secondary thread computs the stats, and terminates,
> 
> and the async framework hands the stats back to
> the main thread that did the await, and the main
> thread stops his waiting and continues to run
> 
> cooperatively with the other tasks in the current
> event loop. The test case measures the wall time.
> The results are:
> 
> > node.js: 10 ms (usual Promises and stuff)
> > JDK 24: 50 ms (using Threads, not yet VirtualThreads)
> > pypy: 2000 ms
> 
> I am only using one main task, sequentially on
> such await calles, with a couple of file, not
> more than 50 files.

So that's 2000 ms for 50 files or 40 ms per file?

This is indeed extremely slow. On my laptop, CPython 3.12 and PyPy 7.3
can stat 10000 files in 40 ms. There is no way my laptop is 10000 times
faster than your machine.

I also measured how long it takes to create and join a thread.
Here CPython with 72 µs is quite a bit faster than PyPy with 150 µs, but
even that should be able to do about 250 create thread/stat/join cycles
(although of course that would be a stupid thing to do). Oh, and lo and
behold, that's about the performance I get when I get with
asyncio.to_thread(os.stat, ...):

#v+
#!/usr/bin/python3
import asyncio
import os
import random
import time

filenames = []
def createfilenames(d, levels):
    if levels == 0:
        for i in range(10):
            filenames.append(f"{d}/f{i}")
    else:
        for i in range(10):
            dn = f"{d}/d{i}"
            createfilenames(dn, levels-1)

async def statfiles(filenames):
    count = 0
    total_length = 0
    t0 = time.monotonic()
    for fn in filenames:
        st = await asyncio.to_thread(os.stat, fn)
        count += 1
        total_length += st.st_size
    t1 = time.monotonic()
    print(f"{count} files with {total_length} bytes stat'ed in {t1 - t0:.3f} seconds")

createfilenames(".", 3)
random.shuffle(filenames)
asyncio.run(statfiles(filenames))
#v-

% pypy3 stat_asyncio
10000 files with 13235076 bytes stat'ed in 1.415 seconds

% python3 stat_asyncio
10000 files with 13235076 bytes stat'ed in 0.788 seconds

        hjp

-- 
   _  | Peter J. Holzer    | Story must make more sense than reality.
|_|_) |                    |
| |   | [email protected]         |    -- Charles Stross, "Creative writing
__/   | http://www.hjp.at/ |       challenge!"
signature.asc (application/pgp-signature, 833 B)
-----BEGIN PGP SIGNATURE-----

iQIzBAABCgAdFiEETtJbRjyPwVTYGJ5k8g5IURL+KF0FAmhb7WcACgkQ8g5IURL+
KF1C+w//SSOdAxHvlvaNhAjGTm5pgPeRYw6l0AufY3UjKCnxvlIhKeXGlrnYV5s/
4JE7EtLlTQLf/8id5rNONgcwrLb+o5xSJYUqpZ8Vbf3Logw/qjx7zNNvDRG2a3MX
kMPpZoUCzmSbYUX1KVPOEJPBd2qdKUiO/OOnxvQoUfgMq+bJ2rLcz0AlzcrVM/Dh
zO9ueOpf2pecVNWyAeUa+5o+fmnssn4gd9Z5KnDxLeUrk/N/FMwE0gSgjNt5G1ky
uIfZbAP/5/tuczyFlFJtZc7RNa1m/43WO4M1C+nfqOmgo1TvQFq6tnRvQny0NGKZ
AL0mlT1NI7JYkT00zUq3ATbeDi1pcOQ/4zA6zVmbI8AgRsbUk/sW10+dMa+mJYuj
nn59SY6vaddHq1VPkTO+9njGIB5GcJ+RGyfhTLiOKMLY7qq9M+JiGgKxdmWriHVZ
CgWd1zFD4w+R268Ih5x8DJEAq+wUd40Altd4DV8NqNk38ymThgCcE7vLGQcJTKH9
Zsdwup1BhyujFBtyEcuHEa+bDCHc+I/gSBEQURMYwJbnKLzjhNjPvTG8T85Kd3Bd
JA/fuzldFomYgLG26JcMCGPKCPaezz6U9ZR05r9svGqSarawarfPZSq664Ljh3G4
5c+b4Rk9dMR+5O4LTIH6Z8LGiqSuSMtOHT7nvthZBYHqMqJ8hgY=
=QGEo
-----END PGP SIGNATURE-----
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.