Click here to Skip to main content
Licence CPOL
First Posted 22 Nov 2011
Views 8,827
Downloads 198
Bookmarked 19 times

Thread Safe Happiness Using a Helper Class

By | 6 Apr 2012 | Article
Local scope delegates and Lambda expressions allow for some very smart and useful utility functions.

Introduction 

Typically we start off coding a project not thinking about making it thread safe. And in many cases, it isn't really necessary. But with the advent of PLINQ and the Task Parallel Library, we find that there is clear performance advantages to thinking 'thread safe' from the beginning. Using classes like ConcurrentDictionary can be a huge time-saver, but we may be handed a collection that needs synchronization that isn't inherently thread safe.

Considerations 

When dealing with repeated possibly time consuming calculations it may be better to run the iterations in parallel.  And for more straight forward arithmetic it is likely better to stay in a single thread.

LazyInitializer  

Happily, there are a few existing classes that help with thread safety. When I can, I initialize my accessor properties using LazyInitializer.EnsureInitialized(ref item, ()=>{}). But this has a few serious limitations:

  1. It does not allow returning null.
  2. It does not inherently allow for synchronizing other properties.

Thread Safe Pattern

After some deepening research, I upgraded my best practices for using locks, and Monitor.TryEnter. I always knew the double check locking (DCL) thread safe pattern to check for a condition, if the condition exists, get (or attempt) a lock, then check the condition again before execution. But until Lambda expressions, I didn't coalesce a way to make utility functions to handle it.

Locking Every Property in a Class

If you have a class that can be accessed by multiple threads, you will need to apply thread safety to all the public properties and likely some of the private ones. I got really tired of writing:

readonly object _propA_lock = new Object();

Optimizing Thread Safe Disk I/O (ReaderWriterLockSlim)

I then wrestled with maximizing file read/write performance. This was much more difficult and was not an absolute solution, but has the potential to seriously upgrade throughput. You still need to catch and retry for I/O exceptions where a file is being accessed outside of your application. I also had a difficult time avoiding I/O exceptions if I cleaned up the ReaderWriterLockSlim objects too aggressively... 

Using the Code

ThreadSafeHelper exposes a set of static methods that act like the lock keyword, but with some extra functionality. Including conditional timeout locking. It also allows for more complex conditions than a simple null value that the LazyInitializer provides. (See "Important Notes" on proper conditional implementation.)

if(!dictionary.ContainsKey(key)) {
  lock(dictionary) {
    if(!dictionary.ContainsKey(key)) {
      /*some code that needs to be thread safe*/
    }
  }
}

Reduces down to: 

ThreadSafeHelper.Execute(dictionary,()=> !dictionary.ContainsKey(key),()=>
 { /*some code that needs to be thread safe*/ });

Or  the more performance optimized read/write version:

ThreadSafeHelper.Execute(dictionary, key, ()=> !dictionary.ContainsKey(key),()=>
 { /*some code that needs to be thread safe*/ },
 5000 /*lock-timeout*/,
 false /*throw on error*/);  

Note: The optimized version above (requires a key) has been tested to perform equally as well applied to Dictionary<TKey,TValue> as it is with ConcurrentDictionary<TKey,TValue>'s built in GetOrAdd method.

...

ThreadSafeHelper can also be initialized as an instance class which automatically and safely creates/leverages locks for you. Here is how I use it as an instance: 

// Default keys are strings but you can use whatever key type you like (int for example).
readonly ThreadSafeHelper ThreadSafe = new ThreadSafeHelper();
void Example() {

  // Method A:
  ThreadSafe.Execute("[keyName]",()=>{ /*some code that needs to be thread safe*/ });

  // Method B:
  lock(ThreadSafe["[keyName]"]) { /*some code that needs to be thread safe*/ }

  // Method C: (Conditional by key)
  ThreadSafe.Execute("[keyName]",()=> {return property==null && !foo},()=>
  { /*some code that needs to be thread safe*/ });
}

Lastly, optimizing file access is easy:

ThreadSafeHelper.File.ExecuteRead(filePath,()=>{
  /* Some code that requires read access of the specified file.
     Allows for multiple readers. But blocks if there is a write lock in progress.
   */
});

ThreadSafeHelper.File.ExecuteWrite(filePath,()=>{
  /* Some code that requires explicit write access of the specified file.
     Blocks all other access to the file until this is complete.
   */
});

Keep in mind you will need to apply a while/try/catch/sleep retry strategy for file access within your local function in case something else accesses the file outside your application. I could have built the exception handling in, but the diversity of possible implementations is too vast to really make this robust within the utility... In some cases, you may be writing a file and an IoException occurs where you need to handle that error and do complex cleanup before continuing. You may not want to retry. I've included a ThreadSafeHelper.File.GetFileStreamForRead method which can assist in typical usage.

Points of Interest 

I left the LOCKCLEANUPDELAY open for you to experiment with and tune. Delaying cleanup seemed to be the happy solution to avoiding file access collisions and worrying about excessive locking. You can set this to zero for it to execute cleanup after every run, but in my tests this is prone to IoExceptions.

Important Notes

With or without the ThreadSafeHelper utility, when implementing a conditional lock (which uses a double check locking pattern), be certain to not alter the condition used until the end of your code block because it will prematurely negate the condition before the synchronized code is finished.

// Example A
object result;
ThreadSafeHelper.Execute(dictionary,()=> !dictionary.TryGetValue(key, ref result),()=>
{
  object temp;
  /* some potentially complex code that may take some time to finish
     and eventually sets/initializes 'temp' */
  
  // Absolute last step:
  dictionary.Add(key,result = temp);
});

// Example B
ThreadSafe.Execute("[keyName]",()=> _value==null,()=>
{
  object result;
  /* some potentially complex code that may take some time to finish
     and eventually sets/initializes 'result' */
  
  // Absolute last steps:
  // System.Threading.Thread.MemoryBarrier(); // memory fence for multiprocessor systems.
  _value = result;
});

In the above examples, if for any reason you added the value anywhere but at the end of the function, you may end up returning that value before it is ready.

Thread.MemoryBarrier() has been suggested to avoid processor reordering issues. From MSDN: "It synchronizes memory. In effect, flushes the contents of cache memory to main memory, for the processor executing the current thread." But apparently the lock keyword (Monitor.Enter/Exit) implicitly creates a full memory fence and therefore Thread.MemoryBarrier() is not needed.

Any questions, comments, criticisms, suggestions, and improvements are very welcome!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

essence

Web Developer

United States United States

Member

Just a crazy developer with crazy ideas of grandure.
 
What's that? Anal probe? NO, I said "Anal CODE!" Get that thing away, and get back to EditPlus.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
QuestionCode Updated Pinmemberessence9:12 6 Apr '12  
GeneralMore in depth synchronization information. [modified] Pinmemberessence10:35 6 Dec '11  
GeneralDo I need to synchronize? Pinmemberessence15:44 5 Dec '11  
AnswerDouble Check Locking Pattern (DCL) [modified] Pinmemberessence14:22 5 Dec '11  
GeneralMy vote of 1 PinPopularmemberWilliam E. Kempf4:27 2 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence13:17 3 Dec '11  
GeneralRe: My vote of 1 PinmemberWilliam E. Kempf17:41 3 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence20:49 3 Dec '11  
GeneralRe: My vote of 1 PinmemberWilliam E. Kempf2:35 4 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence7:23 5 Dec '11  
GeneralRe: My vote of 1 PinmemberWilliam E. Kempf8:37 5 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence10:34 5 Dec '11  
GeneralRe: My vote of 1 PinmemberWilliam E. Kempf11:11 5 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence14:24 5 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence15:16 5 Dec '11  
GeneralRe: My vote of 1 PinmemberWilliam E. Kempf2:48 6 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence7:18 6 Dec '11  
GeneralRe: My vote of 1 PinmemberWilliam E. Kempf8:36 6 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence9:26 6 Dec '11  
GeneralRe: My vote of 1 PinmemberWilliam E. Kempf16:33 6 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence13:06 7 Dec '11  
GeneralRe: My vote of 1 PinmemberWilliam E. Kempf7:12 8 Dec '11  
GeneralRe: My vote of 1 PinmemberPaulo Zemek9:44 6 Dec '11  
GeneralRe: My vote of 1 Pinmemberessence10:33 6 Dec '11  
GeneralRe: My vote of 1 PinmemberPaulo Zemek10:38 6 Dec '11  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web01 | 2.5.120517.1 | Last Updated 6 Apr 2012
Article Copyright 2011 by essence
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid