Published: 17 Jul 2009
By: Karl Seguin

The power of threads lie in their ability to share information - but two threads accessing the same information at the same time can have devastating effects.

Contents [hide]

Introduction

Depending on what you're trying to accomplish with threads, you may have to worry about two or more threads trying to access the same memory. At first, that might seem like an edge-case, especially if you're looking at threads for simple or isolated situations - such as unblocking the UI or a specific long-running query. However, as you move more and more of your execution pipeline into a multi-threaded paradigm, a growing number of resources will need to be shared across multiple threads. We'll examine two mechanisms that can help solve this problem: mutual exclusion and atomic operations.

Thread Safety

How serious is it when two threads access the same resources? In some cases it'll be benign, in others completely catastrophic. It depends on what you are trying to do - and whether or not the objects and methods you are using are inherently thread-safe.

First though, understand that when you call a method, say Add on an List, multiple operations are actually executed. First, Add itself is written in .NET and composed of a handful of lines of C# code. This code is then compiled into multiple lines of intermediate language and again into many lines of assembly. This abstraction makes programming much easier, but also much more deceptive. How does this relate to multi-threaded programming? At any point during the execution of Add, the executing thread can be interrupted and another thread given priority. This could leave the List is an unpredictable state. This is the line of code that actually adds the item to the list:

Don't let the fact that all this happens on a single line fool you. The executing thread could be put to sleep after incrementing the size field. If another thread then accessed size, say by calling Count, you'd likely end up with a NullReferenceException in your code as you tried to access an item which didn't yet exist in the list.

Essentially, what we're talking about here is atomicity. An atomic operation is one that cannot be interrupted and will either succeed or fail. As you start to do more multi-threaded programming you'll quickly learn that the vast majority of methods in .NET aren't atomic. As such, you need to know what is and isn't thread-safe. Luckily, the MSDN consistently documents the thread-safety of each class and method. If we look at the documentation for the List<T> class, we'll find a section named "Thread Safety", which states:

Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

A List<(Of <(T>)>) can support multiple readers concurrently, as long as the collection is not modified. Enumerating through a collection is intrinsically not a thread-safe procedure. In the rare case where an enumeration contends with one or more write accesses, the only way to ensure thread safety is to lock the collection during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.

Locking

The primary way to turn thread-unsafe code into thread-safe code is to lock access to shared variables as you use them. There are many ways to lock code, but they all do the same thing: give you control over how many threads execute a given piece of code. For what we'll look at, we'll always give one thread exclusive access, but know that, if you have to, you can give multiple threads access.

The simplest example we can consider is a counter which tracks the number of hits to our website. Our implementation will simply rely on a static integer counter which we'll increment whenever our application's BeginRequest method fires (in global.asax.cs). Our first version is as simple as possible

Listing 1: Naïve implementation of a hit counter

Despite the elegant simplicity, the above code won't cut it - multiple threads can execute BeginRequest at the same time, thus trying to increment _hitCount at the same time. Unfortunately, the pre-increment (++) operator isn't thread safe which means we'll get some unexpected results. What we need to do is synchronize access to our variable. This can be accomplished in many ways, the most common being with the use of Monitor.Enter and Monitor.Exist methods (found in the System.Threading namespace):

Listing 2: A better hit counter

Before we examine the above code, let's leverage the C# lock statement which provides a more convenient syntax to the above code (much like the using statement does). The following code compiles into the above:

Listing 3: A even better hit counter

(VB.NET users should look at the SyncLock statement)

The above code create an exclusive lock on a static object (statics are shared across all threads). Locking is an atomic operation and only 1 thread can hold the lock. Since unlocking happens within a finally you don't need to worry about exceptions causing your locks to be held forever. If you want to learn more about why we lock on a static object, check out: http://www.yoda.arachsys.com/csharp/threads/lockchoice.shtml.

Locking Pitfalls

Without question, locking is something that you'll end up having to do a lot of when doing multi-threaded programming, which means you need to be skilled at avoiding some of the very real and very dangerous risks associated with it. First and foremost, remember that our goal with multithreaded programming is to improve performance and scalability; however, locking does the exact opposite - it serializes access to our code. Therefore, the most important thing you must do is carefully review the code being locked and, as much as possible, minimize it. For example, say we want to celebrate our 1000000 hit, we might be tempted to do something like:

Listing 4: Our 1 millionths hit!

But we could tweak it slightly to reduce the time spent in our lock:

Listing 5: Our 1 millionths hit (improved)!

This may seem like an overly simplistic example, but it shows how locally scoped members can be used to reduce lock durations. Oftentimes, you'll have to give a lot more thought to what has to and shouldn't be locked.

The other danger to look out for when locking are deadlocks. If you have multiple locks on multiple objects, it's possible to completely freeze your code. For example, imagine something like:

Listing 6: DEADLOCK Ahead!

Things can get nasty if a thread acquires a lock on _lockB while another thread acquires a lock on _lockA. Each thread will be blocked, waiting forever for each other to release their respective locks. The only solution to such code is to rearrange the code to remove the possible deadlock.

Lock Free

It is possible to write multi-threaded code without the use of locks. Such an approach generally revolve around the use of atomic operation. If an operation is guaranteed to execute atomically at the CPU level then we don't need to worry about locking exclusive access to our resources. For example, our hit counter could leverage .NET's Interlock class which is used specifically for atomic operations:

Listing 7: Atomic Hit Counter

We haven't talked much about it, but most of the multithreaded locking you'll end up doing will be around collection operations (adding items to queues, removing them, peeking, and getting counts). The simplest approach is to lock the SyncRoot property of the collection itself before any operations:

Listing 8: Collection Locking

(The .NET framework has some built-in collection which are thread-safe (using the above method)).

Depending on what you are doing, the above code might not scale well and prove to be a serious bottleneck for your entire system. A solution might be to use lock-free collections. One of the better (and free) set of lock-free collections is from Julian Bucknall. His collections leverage the CPU's Compare and Swap instruction. If you are interested, I suggest you check out his blog for more details (as well as a link to download his code).

.NET 4.0

It's worth pointing out that .NET 4.0 has a System.Collections.Concurrent namespace which will ease some of grunt work we must deal with (although reviews of the beta suggest that they use locks making them unsuitable for high throughput systems).

Conclusion

In this part we covered the foundations of multi-threading programming, namely locking and lock-free operations. We also looked at some of the common locking pitfalls, which can easily bring a system to its knees (either by serializing access or creating deadlocks). Be warned that multi-threading programming is an advanced topic highly dependent on exactly what it is you are doing, and even on what type of system you are doing it.

<<  Previous Article Continue reading and see our next or previous articles Next Article >>

About Karl Seguin

Karl Seguin is an senior application developer at Fuel Industries, located in Ottawa, Ontario. He's an editor here at DotNetSlackers, a blogger for the influential CodeBetter.com and a Microsoft MVP.

This author has published 8 articles on DotNetSlackers. View other articles or the complete profile here.

Other articles in this category


Developing a Hello World Java Application and Deploying it in Windows Azure - Part I
This article demonstrates how to install Windows Azure Plugin for Eclipse, create a Hello World appl...
Android for .NET Developers - Building a Twitter Client
In this article, I'll discuss the features and capabilities required by an Android application to ta...
Ref and Out (The Inside Story)
Knowing the power of ref and out, a developer will certainly make full use of this feature of parame...
Developing a Hello World Java Application and Deploying it in Windows Azure - Part II
In this article we will see the steps involved in deploying the WAR created in the first part of thi...
Android for .NET Developers - Using Web Views
In this article, I'll show a native app that contains a web-based view. The great news is that HTML ...

You might also be interested in the following related blog posts


GiveCamps Get a new Sponsor read more
Scenarios for WS-Passive and OpenID read more
More On The CodePlex Foundation read more
ASP.NET MVC and the templated partial view (death to ASCX) read more
Win a Govie Award Submit an Innovative Gov 2.0 Application read more
The web vs. the fallacies of distributed computing read more
Testability and TDD are not reasons to use the ASP.NET MVC Framework read more
Testability and TDD are not reasons to use ASP.NET MVC read more
VideoWiki - Step 0 read more
Trip Report from Gov 2.0 Camp read more
Top
 
 
 

Please login to rate or to leave a comment.

Free Agile Project Management Tool from Telerik
TeamPulse Community Edition helps your team effectively capture requirements, manage project plans, assign and track work, and most importantly, be continually connected with each other.