Statics & Thread Safety

类别:.NET开发 点击:0 评论:0 推荐:
source:

http://odetocode.com/Articles/313.aspx

begin:

A reoccurring conversation in the newsgroups revolves around the thread safety of static methods (Shared in VB.NET). Is a static method thread safe? Is a static method a bottleneck? These questions are important questions to answer, because the integrity, quality, and performance of your software will depend upon the answers. In this article we will examine the behavior of a static method to gain a better understanding of how the method works.

First, let’s go shopping.

Let’s say I take a grocery cart of 10 items into a checkout lane at my local grocery store. The clerk takes each item from my cart and computes a total cost for all the items. Unless there was a human error (or an item was priced incorrectly), we would expect the clerk’s cash register to compute the correct total.

The clerk is like a thread. Like a thread in an application, the clerk executes a set of instructions to compute the total cost of my grocery items. The clerk will process one cart of items at a time, and each will make use of a cash register to tally the items.

Now picture a different scenario. Picture 5 checkout lanes, each lane with one clerk, but only one shared cash register for the entire store. If multiple clerks are ringing up items on the shared cash register for different shoppers at the same time, nobody will receive the correct total. My clerk might ring up two items and then pause for a second, which allows a second clerk to come in and add another shopper’s item to the current list in the cash register. The total cost for items in my cart would be incorrect. This is an analogy of what can happen if a method is not thread safe – corrupted data.

The solution to the shared cash register problem is to ensure a clerk will have exclusive possession of the single cash register while totaling my order. No other clerks are allowed to use the register until my 10 items are finished. These steps will produce the correct results, but as you might imagine, it can make for long lines at the checkout. Everyone is waiting on this one cash register to come free. This is an analogy of what can happen to the scalability of an application when threads are locked while waiting for a shared resource.

There is a delicate balance to strike when writing code that is both high performance and thread safe with shared resources. Many books and research appears are devoted to the topic, so let’s just start with some basics. When is a method thread safe, and why is everyone so concerned with the static and Shared keywords? Let’s first understand what differentiates a static class member from an instance member

Static and Instance members

Marking a class member as static or shared means the member is associated with the type (the class), and not an instance of the type. Syntactically, a non-static method is only available if you have an instance of a class. Let’s pretend we have a class by the name of Store, with a non-static method OpenForBusiness.

Store groceryStore = new Store(); groceryStore.OpenForBusiness();

Now let’s make the OpenForBusiness method static. We can only invoke the method using the type name.

Store.OpenForBusiness();

This last example should look odd. We are invoking OpenForBusiness on a type by using the class name. How do we know what store it will open? Will it open all stores? Static methods typically go against object oriented programming designs because they do not apply behavior to a specific object - this is a big semantic difference, a difference in meaning. In many cases you’ll see static methods as shortcuts to common functionality. Static methods can encapsulate instructions you want to execute without creating an instance of a class. The .NET libraries contain many examples of static shortcut methods, like File.Open and String.Format.

Although there are syntactic and semantic differences between static and non-static methods at compile and design time, underneath the covers a static method is just like any other method (there are some simplifications in this paragraph, but stick with me). The method encapsulates instructions. Threads execute these instructions. These instructions exist in only one place in memory. It makes no difference how many objects are created, or how many threads are executing the instructions, they only exist once, which is a good thing considering how many threads are running on my machine right now. This is how instructions work, data works very differently.

Static Fields

Now think for an moment about fields (variables) inside of a type, like a simple integer field. If the field is non-static, the field will exist for every instance of the class. If we add an integer field named ID to our Store class, every store will have an ID.

If you mark a field as static, only one instance will ever exist, regardless of how many objects are instantiated (this single instance can exist even when zero instances are created). If the ID field in our store is marked as static, there is only one place to store an ID number for all stores. It’s like the shared cash register in the beginning of the article. It’s not the static methods that you have to watch out for – it’s the static fields. With that thought in mind, let’s dig into some code in

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.

 

By K. Scott Allen

本文地址:http://com.8s8s.com/it/it41816.htm