Question

I recently ran into a race condition while accessing a configuration setting. After examining what I could of the code, I came to the conclusion that the Configuration class' laziness1 was the source of the race condition. Pondering that lead me to me wonder if laziness naturally yields a greater chance of race conditions?


Here's how I came to the initial finding:
The Configuration class is implemented as a static class, which means:

The program cannot specify exactly when the class is loaded. However, it is guaranteed to be loaded and to have its fields initialized and its static constructor called before the class is referenced for the first time in your program.

Which, if I read it correctly, is a long way of saying that the class is lazily loaded. I did not find anything referencing ordering of events when using additional resources. i.e. It didn't say when the configuration file (app.config) would be read.

And while the class itself states the following for thread safety, it's not clear when it actually reads the configuration file.

Any public static members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

The bit about Any instance members are not guaranteed to be thread safe is particularly confusing since a static class cannot be instantiated.

What I believe occurred is that my program wrote to a section of the Configuration which was subsequently over-written as the static instance completed it's initialization and reading from the configuration file.

All of that led me to believe that the lazy loading of the static class with its indeterminate reading of the configuration file created the race condition I smacked into.

Which leads me to my broader question of: does laziness within an application naturally yield a greater chance of race conditions?

1 The particular class in question is System.Configuration.Configuration from the .NET framework which is defined as public static class ConfigurationManager.


For the gory details, this is the abstract version of the code:

//This first line probably isn't required, but I inherited the code and didn't test removing it.
System.Configuration.Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.ConnectionStrings.ConnectionStrings[DBName].ConnectionString = passedParam;
config.Save();
...
ConnectionStringSettings connectionString = ConfigurationManager.ConnectionStrings[DBName];

The answers from here hint at my question, but don't directly address my concern with race conditions. And this one is somewhat related but more focused on IoC.

Was it helpful?

Solution

Your first link does address the issue - it's laziness combined with side effects that is problematic, because the order of side effects matters.

For pure computations, the main thing that changes is space/time usage and whether the computation produces a result or not. More specifically, lazy evaluation won't evaluate expressions that aren't used, so the time usage can be lower, but the space usage is very hard to reason about.

For pure computations that don't complete successfully, both evaluation strategies can give different results. For example, since lazy evaluation doesn't evaluate expressions that aren't used, if an unused expression would've resulted in an exception or an infinite loop, the computation will still terminate.

If the computation is both pure and total (always terminates successfully) both evaluation strategies give the same results (though space/time usage may still be different).

In short, while laziness may require you to think differently to figure out the execution order, it won't result in race conditions unless it's combined with side effects.

OTHER TIPS

Does laziness within an application naturally yield a greater chance of race conditions?

Of course it does. Properly written multithreaded code will not have race conditions, of course; race conditions are caused by not thinking about all the possible outcomes of ordering between multiple threads. This is not to say, however, that lazy loading directly causes race conditions, it just provides one more potential failure point that needs to be considered and accounted for.

In the case of your example, your code didn't do any sort of verifyValidState in the Configuration class, and the laziness led to a race condition.

To put all this another way, one of the axioms of how to write code that avoids race conditions is IMMUTABILITY. Here's one tutorial among many that discusses this subject. By definition, a class that's lazily loaded is not immutable: it has the pre-initialization value, and then it morphs into the loaded value.

Therefore, I would advise you to exercise additional caution when using lazy loading in multithreaded applications.

Licensed under: CC-BY-SA with attribution
scroll top