6

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.

8
  • 2
    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 (e.g. there are cases where evaluating eagerly will get you stuck in an infinite loop while laziness terminates, and cases where eager evaluation raises exceptions that laziness doesn't), but if both strategies terminate successfully the result is the same.
    – Doval
    Commented Nov 5, 2014 at 19:22
  • in my experience, data races typically follow difficulties for programmer to reason about program execution. In that sense, partial laziness seems to often make it worse. On the other hand, consistently implementing laziness somehow makes it easier to me to reason about execution
    – gnat
    Commented Nov 5, 2014 at 19:25
  • 1
    @gnat - I agree with what your saying, and I was more than a little surprised at what I found. Granted, I didn't dig into the source to verify things, but I did walk the code in a debugger and was able to catch it at least once in that state.
    – user53019
    Commented Nov 5, 2014 at 20:15
  • 1
    As an aside from the actual answer, .NET statics are a fairly error-proof form of lazy loading. They're guaranteed to be populated by the end of their first use, so can only really break if their initialization triggers a cycle that references them again before the end of their first use. I would not expect the use you describe to cause issues with the configuration unless you're doing trickery in static constructors.
    – Telastyn
    Commented Nov 5, 2014 at 23:40
  • 1
    @Telastyn: I would add that static field initialization is fine to initialize read-only data, but only if the initialization does not involve network communications or lengthy I/O. If so, it can be difficult to debug. Commented Nov 7, 2014 at 15:38

2 Answers 2

5

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.

3
+100

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.

1
  • 2
    given that a lazily loaded class may not have any data, I would not say that it was mutable by definition. Lazily loaded simply means the presence of the class is a mutable state, not that the class is mutable (whether it is static or not). It is also mutable state that is typically handled by the compiler and/or environment, and not the application.
    – jmoreno
    Commented Dec 18, 2014 at 2:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.