Many of the classes in the .NET framework have the following remark in the “Thread Safety” section: "Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe."
Does this mean static methods are inherently thread safe? The answer is no. Classes with the above note will have thread safe static methods because the Microsoft engineers wrote the code in a thread safe manner, perhaps by using locks or other thread synchronization mechanisms (to allow only one clerk on a shared register at a time).
Now you might be asking: does this mean static members are never thread safe without locks? The answer is no. It’s impossible to know unless the class comes with documentation or you can look at the source code, but a static method can be thread safe without using any locks whatsoever. Let's see how.
A Static Code Example
As an example, let’s work with the following class that represents something to buy at a grocery store.
class GroceryItem { public GroceryItem(string name, float price) { Name = name; Price = price; } public string Name; public float Price; }
These items will be kept in a Cart class. The class, shown next, isn’t very useful because we cannot add or remove items, but it works well for this example.
class Cart { public Cart() { GroceryItems = new GroceryItem[10]; GroceryItems[0] = new GroceryItem("Milk", 3.99F); GroceryItems[1] = new GroceryItem("Bread", 2.50F); GroceryItems[2] = new GroceryItem("Cheese", 3.00f); GroceryItems[3] = new GroceryItem("Eggs", 2.25F); GroceryItems[4] = new GroceryItem("Soda", 1.12f); GroceryItems[5] = new GroceryItem("Candy", 0.80f); GroceryItems[6] = new GroceryItem("Lettuc", 5.50f); GroceryItems[7] = new GroceryItem("Tomato", 4.00f); GroceryItems[8] = new GroceryItem("Fish", 12.00f); GroceryItems[9] = new GroceryItem("Cereal", 5.00f); } public GroceryItem[] GroceryItems; }
Finally, we will have a static method to total the cost of items in a cart. We will use a call to the Sleep method to slow the computation down and make sure there are multiple threads inside the for loop simultaneously.
class CheckoutLane { public static float GetTotal(Cart cart) { float total = 0; for (int i = 0; i < cart.GroceryItems.Length; i++) { total += cart.GroceryItems[i].Price; Thread.Sleep(100); } return total; } }
Next, we run code below to see what happens. This code will create an array of three threads, and populate the array with three new Thread objects. Each thread is initialized to run the Sum method, and then each thread starts. Finally, we wait for each thread to exit with a call to Join. Inside the Sum method, a new Cart is created (and remember our carts will populate themselves with items), and the static GetTotal method is used to compute the total cost of the items in the cart.
static void Main(string[] args) { Thread[] threads = new Thread[3]; for (int i = 0; i < threads.Length; i++) { threads[i] = new Thread(new ThreadStart(Sum)); } for (int i = 0; i < threads.Length; i++) { threads[i].Start(); } for (int i = 0; i < threads.Length; i++) { threads[i].Join(); } } static void Sum() { Cart cart = new Cart(); Console.WriteLine("Items total {0}.", CheckoutLane.GetTotal(cart)); }
The output will be:
Items total 40.16. Items total 40.16. Items total 40.16.
So far, so good. There will be three threads executing inside this method at the same time. Each thread will have it’s own Cart object that the thread created in the Sum method. Each thread will also have a local variable (total) to keep the sum. Local variables are variables declared inside of a method, and local variables exist on the stack, which is an area of temporary memory or ‘scratch memory’ reserved by each thread (note: for reference objects, the thread will store the value of the reference variable on the stack, but the value references an object on the heap). It’s useful to think of local variables as being private to a thread of execution.
Think of the thread as a clerk. Each clerk totals the items from a single cart on their own, dedicated cash register. We are not sharing any resources (except the single set of instructions being executed, but that happens all the time and is not a concern – it’s the data, remember?)
Let’s slightly tweak the CheckoutLane class and rerun the program to see what happens.
class CheckoutLane { static float total; public static float GetTotal(Cart cart) { total = 0; for (int i = 0; i < cart.GroceryItems.Length; i++) { total += cart.GroceryItems[i].Price; Thread.Sleep(100); } return total; } }
We have made the total variable a static member of the CheckoutLane class. There is exactly one storage location for total across all instances of the CheckoutLane class. Execute this program and we might see:
Items total 55.04. Items total 61.16. Items total 88.46.
Now we have some inconsistencies. Using a static field is like sharing a cash register – you can’t let more than a single person on the register at a time. There are three threads inside the method and they are all accumulating the total cost inside of a single, shared storage location. Now we have a problem with multi-threading. We can fix the problem by using a lock:
class CheckoutLane { static float total; static object synchLock = new object(); public static float GetTotal(Cart cart) { lock(synchLock) { total = 0; for (int i = 0; i < cart.GroceryItems.Length; i++) { total += cart.GroceryItems[i].Price; Thread.Sleep(100); } return total; } } }
The above code will produce the correct results again, because only a single thread is allowed inside the lock. The lock prevents two or more threads from overwriting each other’s total. If you time the execution of the program, you’ll notice it takes a little longer to execute. Since the threads are executing one at a time inside the method in this code, the extra time makes sense.
Will methods using locks, monitors, and mutexes be slower than methods that do not? The answer is: in general, yes. However, it is dangerous and almost impossible to eyeball a piece of multi-threaded code and determine the performance characteristics. You must test the code with your application and in your execution environment to determine the performance characteristics for your workload. Any other approach will be an educated guess.
Sharing A Conclusion
I hope this article has been an enlightening view on the subject of static members and thread safety. Watch for thread safety when static fields are in play. You’ll generally find static fields are put to use inside of static methods, which might be the reason static methods are always a concern in a multi-threaded application. Just remember it’s not the static methods you have to watch for, it’s the static data.