Thread
-
pg_dump: use threads for parallel workers on all platforms
Bryan Green <dbryan.green@gmail.com> — 2026-07-02T16:30:39Z
pg_dump runs parallel workers in two completely different ways. On non-Windows platforms, they're forked processes communicating with the leader over pipes-- which is what pipes and processes are for. Windows has no fork(), so there the workers are threads. But instead of coordinating those threads as threads, the Windows port runs the same process-based protocol on top of them, unchanged. Each worker's channel to the leader is a loopback TCP socketpair on 127.0.0.1, opened when the worker starts. So to tell a worker "dump table 1234," the leader serializes the command to a string and writes it down that socket; the worker reads it back a byte at a time, and the leader watches the sockets with select() to see who's done. All of it to hand work to a thread a few megabytes away in the same address space. And because the leader waits on the workers with WaitForMultipleObjects, which takes at most 64 handles, you can't run more than 64 jobs on Windows. The parallelism limit there is a limit of the wait call. (The non-Windows side doesn't have this limit-- it reaps workers with wait() rather than WaitForMultipleObjects, so PG_MAX_JOBS is INT_MAX.) None of this is broken; it works. It's threads pretending to be processes because the code was written for processes, and the port kept the protocol rather than rethinking it. I'd like to stop. One model everywhere should be threads on all platforms, coordinated by an in-process work queue-- a mutex and a couple of condition variables-- instead of two worker models bridged by an inter-process protocol. To be clear, the unification is on the queue, not on what Windows does today. Teaching the non-Windows side to talk to its own threads over a socket, a byte at a time, would just be the same trick on more platforms-- that's the part worth deleting, not copying. I've done the Windows half, both to prove it out and because it's the coordination layer the non-Windows side would adopt. The socket protocol is gone; the leader hands work to a worker in memory instead of down a loopback connection. The 64-job cap is gone. The unchecked _beginthreadex return-- which on failure recorded a thread that didn't exist as an idle worker-- is fixed. Dump and restore are byte-for-byte identical to stock from -j2 through -j250. The non-Windows port to threads isn't written yet, and I won't start it until the direction is settled. One piece I deliberately left for the unified version: the queue still passes the command as a string-- "DUMP 1234"-- and the worker parses it back and looks the ID up to recover the TocEntry it already came from. In one address space that's ceremony; the queue could carry a { T_Action, TocEntry * } and drop the serialize/parse/lookup entirely. I didn't do it on Windows because the non-Windows side still forks, and a pointer is meaningless across a process boundary. There, the string is the serializer that path needs, so converting it to Windows-only would add a second message format instead of removing one. With threading in place across Windows and non-Windows, we can pass the work item directly and delete buildWorkerCommand/parseWorkerCommand and the matching response pair, plus the dumpId lookup, on every platform at once. The cost is crash isolation. fork() gives each non-Windows worker its own address space, so one that segfaults can't corrupt the leader or its siblings; threads give that up. What it actually buys today is narrow. The moment any worker dies, the leader pg_fatal()s and the whole dump comes down, so processes don't give you recovery-- only the guarantee that a corrupt worker can't scribble on a sibling's output before everyone exits. Windows has run without even that for years. It's an acceptable trade for a single implementation, but it's the real cost. I'll say plainly that this fixes no user-visible bug, and nothing is broken today. It's consolidation. There are two implementations of parallel dump right now, and they drift: the Windows one grew the socket emulation and the 64-job cap out of running a process protocol on threads, and it carried bugs the non-Windows side never had, like the _beginthreadex one above. One model means a fix or a feature lands once, on a path exercised on every platform, instead of twice, with a seam down the middle. Windows already shows the threaded model works here, and threads are the half both sides can share, since Windows can't fork. The Windows port originally kept the process shape to stay consistent with the other platforms; this keeps that same goal, on the model that actually ports. The strongest evidence of the thread-safety of the worker-reachable code-- Windows has run that path with threads for years. With the thread rework, fmtId's static return value, is now _Thread_local. The global state a worker reads is built during the single-threaded catalog phase, before any worker exists. Patches are attached-- 0001 and 0002 are independent and can be committed separately; 0004 depends on 0003. -- Bryan Green EDB: https://www.enterprisedb.com -
Re: pg_dump: use threads for parallel workers on all platforms
Heikki Linnakangas <hlinnaka@iki.fi> — 2026-07-05T23:21:56Z
On 02/07/2026 19:30, Bryan Green wrote: > None of this is broken; it works. It's threads pretending to be > processes because the code was written for processes, and the port kept > the protocol rather than rethinking it. I'd like to stop. > > One model everywhere should be threads on all platforms, coordinated by > an in-process work queue-- a mutex and a couple of condition variables-- > instead of two worker models bridged by an inter-process protocol. +1 > To be clear, the unification is on the queue, not on what Windows does > today. Teaching the non-Windows side to talk to its own threads over a > socket, a byte at a time, would just be the same trick on more > platforms-- that's the part worth deleting, not copying. > > I've done the Windows half, both to prove it out and because it's the > coordination layer the non-Windows side would adopt. ... I see why you developed this that way, and it makes a lot of sense. However, it has one downside: the Windows-only code cannot be tested without Windows. At quick glance, it looks reasonable, but I'm a little nervous committing more Windows-only code without being able to easily play with it myself. Once you have the final patch ready to switch non-Windows systems to threaded model too, that gets easier. > With the thread rework, fmtId's static return value, is now > _Thread_local. +1. This is the first _Thread_local in our codebase. It's in C11, so it should just work, but we'll see if the buildfarm shows any surprises... With this, the getLocalPQExpBuffer hook is never set. I think we can just remove it, and rename defaultGetLocalPQExpBuffer() to getLocalPQExpBuffer() directly. I noticed that we currently call setFmtEncoding() in multiple places in src/bin/pg_dump. I haven't looked at them closely, but I wonder if there's some kind of thread-safety hazards there even without these patches. And what about the 'quote_all_identifiers' global variable? Is that set correctly in both models? I guess it's inherited through fork(). - Heikki
-
Re: pg_dump: use threads for parallel workers on all platforms
Heikki Linnakangas <hlinnaka@iki.fi> — 2026-07-07T15:25:00Z
On 02/07/2026 19:30, Bryan Green wrote: > The unchecked _beginthreadex return-- which on failure recorded a > thread that didn't exist as an idle worker-- is fixed. Committed and backpatched this patch (0001) now, to get that out of the way. The rest remain. Thanks! - Heikki