Threading issues (which I've also been worrying about lately) arise from the use of multiple processor cores with separate caches, as well as from basic thread-swapping race conditions. If the caches for separate cores access the same memory location, they will generally have no idea about the other one and may separately track the state of that data location without it going back out to main memory (or even to a synchronized cache shared across all cores at L2 or L3, for example), for processor performance reasons. So even order-of-execution interlock tricks may not be reliable in multi-threaded environments.
As you may know, the main tool to correct for this is a lock, which provides a mechanism for exclusive access (between contentions for the same lock) and handles the underlying cache synchronization so that accesses to the same memory location by various lock-protected code sections will be properly serialized. You can still have race conditions between who gets the lock when and in what order, but that's usually much simpler to deal with when you can guarantee that execution of a locked section is atomic (within the context of that lock).
You can get a lock on an instance of any reference type (eg. inherits from Object, not value types like int or enums, and not null), but it's very important to understand that the lock on an object has no inherent effect on accesses to that object, it only interacts with other attempts to get the lock on the same object. It is up to the class to protect access to its member variables using an appropriate locking scheme. Sometimes instances might protect multi-threaded accesses to their own members by locking on themselves (eg. lock (this) { ... }
), but usually this is not necessary because instances tend to be held by only one owner and don't need to guarantee threadsafe access to the instance.
More commonly, a class creates a private lock (eg. private readonly object m_Lock = new Object();
for separate locks within each instance to protect access to members of that instance, or private static readonly object s_Lock = new Object();
for a central lock to protect access to the class's static members). Josh has a more specific code example of using a lock. You then have to code the class to use the lock appropriately. In more complex cases you might even want to create separate locks for different groups of members, to reduce contention for different kinds of resources which aren't used together.
So, to bring it back to your original question, a method which only accesses its own local variables and parameters would be thread-safe, because these exist in their own memory locations on the stack specific to the current thread, and can not be accessed elsewhere--unless you shared those parameter instances across threads before passing them.
A non-static method which only accesses the instances own members (no static members)--and of course parameters and local variables--would not need to use locks in the context of that instance being used by a single owner (doesn't need to be thread-safe), but if instances were intended to be shared and wanted to guarantee thread-safe access, then the instance would need to protect access to its member variables with one or more locks specific to that instance (locking on the instance itself being one option)--as opposed to leaving it up to the caller to implement their own locks around it when sharing something not intended to be thread-safe shareable.
Access to readonly members (static or non-static) which aren't ever manipulated is generally safe, but if the instance it holds is not itself thread-safe or if you need to guarantee atomicity across multiple manipulations of it, then you may need to protect all access to it with your own locking scheme as well. That's a case where it could be handy if the instance uses locking on itself, because you could simply get a lock on the instance across multiple accesses into it for atomicity, but you wouldn't need to do so for single accesses into it if it's using a lock on itself to make those accesses individually thread-safe. (If it's not your class, you'd have to know whether it locks on itself or is using a private lock which you can't access externally.)
And finally, there's access to changing static members (changed by the given method or by any others) from within an instance--and of course static methods which access those static members and could be called from anyone, anywhere, anytime--which have the biggest need to use responsible locking, without which are definitely not thread-safe and are likely to cause unpredictable bugs.
When dealing with .NET framework classes, Microsoft documents in MSDN whether a given API call is thread-safe (eg. static methods of the provided generic collection types like List<T>
are made thread-safe while instance methods may not be--but check specifically to be sure). The vast majority of the time (and unless it specifically says it's thread-safe), it's not internally thread-safe, so it's your responsibility to use it in a safe manner. And even when individual operations are implemented internally thread-safe, you still have to worry about shared and overlapping access by your code if it does anything more complex which needs to be atomic.
One big caveat is iterating over a collection (eg. with foreach
). Even if each access to the collection gets a stable state there's no inherent guarantee that it won't change in between those accesses (if anywhere else can get to it). When the collection is held locally there's generally no problem, but a collection which could be changed (by another thread or during your loop's execution!) could produce inconsistent results. One easy way to solve this is to use an atomic thread-safe operation (inside your protective locking scheme) to make a temporary copy of the collection (MyType[] mySnapshot = myCollection.ToArray();
) and then iterate over that local snapshot copy outside the lock. In many cases this avoids the need for holding a lock the whole time, but depending on what you're doing within the iteration this may not be enough and you just have to protect against changes the whole time (or you may already have it inside a locked section guarding against access to change the collection along with other things, so it's covered).
So, there's a bit of an art to thread-safe design, and knowing just where and how to get locks to protect things depends a lot on the overall design and usage of your class(es). It can be easy to get paranoid and think you have to get locks all over for everything, but really it's about finding the right layer at which to protect things.