Posted On: 2019-11-04
Race conditions are a particularly troublesome category of bug - they are notoriously inconsistent, and the behavior they produce often does not provide any information about the actual underlying issue. Undetected race conditions sometimes go unresolved for years, or even indefinitely - some companies require reliable replication steps before investigating issues, and race conditions, by their definition, are unreliable. As parallelization and asynchronous software become increasingly common, the likelihood of writing software containing race conditions increases. Yet, for all these issues, race conditions aren't terrible monsters - rather, they are often quite mundane: simple mistakes that can be easily remedied.
Despite their troublesome nature, race conditions are actually conceptually simple. Any time two (or more) actions are running at the same time, and the final outcome changes based on which one finishes first then you have a race condition. As a real-world example of this: imagine an airplane pilot and a passenger, both of which are running late for the same flight. When the passenger arrives, they will board the plane and wait for take-off. When the pilot arrives, the flight staff will close the gate (preventing boarding) and prepare for take-off. In this situation, if the passenger arrives first, the passenger will board the plan, but if the pilot arrives first, then the passenger won't be able to board, whenever they do arrive. Thus, who is on the plane when it takes off depends upon the order in which the two "racing" people arrive*.
Historically, most software race conditions have been avoided simply by executing in sequence. This allowed code to be written relatively simply, with the code itself enforcing assumptions about which one completes before the other. To use the real-world example: if the only way to the airplane gate was by taking a shuttle, and the shuttle always took the passenger first and then the pilot, then there is no race - the pilot always arrives last, and the plane always takes off with the passenger on board.
Certain applications (such as dynamic web pages) experience significant performance degradation when programmed synchronously. In its most extreme (such as ajax with synchronous flag set to true) the user may experience the application freezing as it waits for the synchronous task to complete. Instead, developers of such applications often need to write code that executes asynchronously, while still creating synchronized end-result outcomes.
Fortunately, there are programming patterns that can vastly simplify these tasks, many of which have readily available implementations in languages that are commonly used with asynchronous tasks. I wrote a bit about this back in February - exploring the options available for use in Unity. Of these, the promise is likely to be the most relevant* for you, as it is now standard in javascript (one of the most popular languages that regularly deals with aynchronicity.)
The promise pattern provides a number of useful solutions to race conditions. In particular, the Promise.all
composition makes it possible to wait for the completion of multiple asynchronous operations.
To look at the real-world example again, the issue at hand is the fact that the pilot's boarding prevents the boarding of the passenger.
Thus, if we change the rules so that the gate doesn't close until all the passengers and staff are on board, then the problem goes away
(though, admittedly, this means that an especially late passenger will delay the flight.)
This has been a pretty high-level overview of race conditions, but hopefully it has demystified them a bit. Like many other common programming problems, a race condition is a relatively simple concept - one that is, fortunately, not too difficult to resolve when using the right tools. While the exact tools available will vary based on language and domain, the Promise api is an excellent tool available for front-end web developers*.