The Silent Menace of Non-Determinism: How Threads and Garbage Collection Undermine Program Reliability

Picture this: Your application runs flawlessly 99.999% of the time, but under rare conditions, it spits out wildly inconsistent results. You’ve just encountered the curse of non-repeatability—a plague inflicted by three pervasive features in modern programming: threads, garbage collection (GC), and non-deterministic destructors. At its core, non-repeatability means identical program runs don’t produce identical internal steps or outputs, even when results should be equivalent. This isn’t merely an academic quirk—it’s a breeding ground for heisenbugs that evade detection and devastate debugging efforts.

Why Non-Determinism Breeds Chaos

Non-repeatability transforms minor flaws into catastrophic failures. Consider a threaded map/reduce operation: Inputs reach the reduce phase in unpredictable orders. If your code harbors a subtle bug—like the infamous Pentium FDIV error—changing sequences might trigger it intermittently. As the source points out:

"Depending on the order of inputs, you might or might not trigger the bug, and your result might be different... debugging it is hard."

Threads amplify this through race conditions. Without precise locking, programs appear correct until a timing fluke corrupts data. GC compounds the issue by introducing randomness in when objects are destroyed, particularly through non-deterministic destructors. These destructors—which run whenever GC reclaims memory—can manipulate other objects, close connections, or alter state unpredictably. The result? A silent cascade of side effects.

The Garbage Collection Trap and the Rise of Manual Mayhem

GC proponents argue destructors should "do as little as possible," but as the source notes, "'as little as possible' is still too much." Real-world resources like database handles or sockets demand timely cleanup, forcing languages like .NET into manual workarounds. The using statement or dispose() pattern emerges—but it’s a band-aid. When objects nest (e.g., a container holding disposable members), encapsulation shatters. Developers must manually propagate dispose() calls, creating boilerplate and risking leaks if references linger.

Java sidesteps this with no deterministic destruction, while Python’s elegant refcounting—where objects like files auto-close when references vanish—is now endangered. As the source laments:

"Python developers have declared deterministic destruction an 'implementation detail'... [because] ports to Java/.NET VMs lack it."

The cost? Inconsistent behavior across environments and creeping complexity with with statements, mirroring .NET’s pitfalls.

Refcounting: A Deterministic Lifeline with Threaded Shackles

Refcounting offers salvation: Objects destroy immediately when references drop to zero, ensuring predictable sequences. Python exemplifies this beautifully—a one-liner like open("filename", "w").write(getpid()) safely creates, writes, and closes a file. But threads break the spell. Synchronizing refcounts across cores introduces heavy overhead, making it impractical for highly concurrent systems. This is why Java and .NET favor GC—it’s theoretically faster and avoids per-assignment synchronization.

Yet, GC’s non-determinism exacts a toll. Under load, resources like sockets may close late, causing mysterious failures. As the source recounts, a C# server leaked connections randomly because dangling references delayed disposal. The fix? Manual refcounting—ironic in a GC-driven ecosystem.

Industry Crossroads: Threads or Sanity?

The push toward multi-core processors pressures developers to adopt threads for performance. But this demands GC and non-determinism, trading reliability for speed. The source draws a sharp parallel:

"Intel is repeating the same trick here; they want us to make our programs harder to write, so their processors don’t have to be harder to design."

This isn’t progress—it’s a regression. Single-threaded, refcounted approaches dominate for a reason: They prioritize developer sanity and debuggability. As concurrency evolves, languages must reconcile determinism with parallelism without sacrificing predictability. Until then, the wisest path may be resisting the siren call of threads for simpler, more reliable paradigms.

Source: Original article by Avery Pennarun