If you write a lock statement you’re doing something wrong.
Now that I have your attention, let me qualify that. If you’re writing business applications and there is any explicit data synchronization code (locks, mutexes, semaphores, etc.) in your business logic, you’re doing it wrong.
It occurs to me that my original statement can be interpreted another way. Using locks correctly is very difficult, even for an experienced programmer. The likelihood that you made a mistake in how you use the lock is very high. That’s not the interpretation I originally intended, but it’s another reason not to use locks if you can avoid them.
I know that some of you will disagree with me. That’s okay. Being wrong isn’t a crime. Continuing to do it wrong even after learning how to do it right isn’t a crime either. It’s just stupid. Using locks to control access to mutable data is wrong. It will lead you down a dark and dangerous path, most likely resulting in the complete failure of your program.
One of the most important lessons we’ve learned over the last 50 years of developing computer programs is that we must strictly control how data is changed. Global variables, although convenient, put your data at risk because any bit of code can change them. The same is true of mutable public fields or properties, which are little more than globals dressed up in an object-oriented wrapper. The risk of something setting an incorrect value or modifying a variable at an inopportune time increases with the number of things that have access to that variable. The risk of something failing because the variable changed increases with the square of the number of things that can modify it.
In a computer program working with thousands or millions of objects, the number of possible failures is frighteningly large. That’s one reason we “hide” objects from each other, and why objects hide their internal implementation details: to reduce the risk that something will inadvertently change a critical value. Strictly controlling access to data makes programs easier to write, easier to change, and more reliable.
We learned those lessons in the days of single processor, single core computers, when the vast majority of programs were single threaded and the computer was doing one thing at a time. The operating system had interrupt routines that ran asynchronously, and multi user operating systems obviously had threading capabilities, but almost all user programs operated on a single thread. It took us decades to learn about the evils of sharing data and to develop programming idioms that minimize the risk.
A typical developer machine today has a single processor with four cores and hyper threading. The computer can be doing eight things at once. Individual programs make increasing use of multiple threads, either cooperating together on a single problem (four threads processing a single large list of items) or each thread handling one part of a multi stage process in a pipelined producer/consumer relationship. This is a Good Thing, except that in writing these applications, programmers are forgetting yesterday’s the hard won lessons.
In our old single threaded programs we focused our attention on keeping data private from other objects. That is, object A was not able to view or change the private implementation details of object B. We learned that a smaller surface area (fewer mutable public properties) meant fewer things could go wrong. But with multithreaded programs we have the potential of multiple threads of execution having access to the private implementation details of a single object. Object A still doesn’t have access to object B’s private parts, but code in object B is being executed by several concurrent threads, all of which have unlimited access to object B’s private parts. This is a recipe for disaster.
Introductory books and articles about multithreading make the mistake of introducing locks, semaphores, events, and other synchronization primitives early on. The descriptions and examples focus on using those primitives (especially locks) to control access to mutable data. After reading those introductory chapters, programmers are left with the impression that the way to “do multithreading” is to wrap their variable accesses in synchronization primitives. Nothing can be further from the truth. The only thing you’ll get from “doing multithreading” that way is a headache: a big monster, eye throbbing, head pounding headache that makes you wish for the mild headache you got when the little kid next door was bouncing his basketball off your bedroom wall while you’re trying to take a nap.
If you’ve done any multithreading work, you’re probably familiar with deadlocks, livelocks, race conditions, lock convoys, and other multithreading hazards. It’s likely that, like most of us, you’ve written your share of those types of bugs. If you’re smart, you’ve decided that mutable data synchronization is a fool’s errand, and you’ve either learned how to do things the right way or you’ve thrown up your hands in disgust at the craziness and gone back to the single threaded world where things make sense more often.
When you start writing locks, you’re forced to think about how your code works rather than what your code does. Writing explicit synchronization in your business logic is like having to write your code in assembly language rather than in C#. It’s possible, but you’ll spend a lot more time on it and the result will be a lot harder to test, debug, and maintain.
How many times have you asked yourself or your coworkers, “Is this code thread safe?” If you have to ask that about your business logic, you’re doing it wrong! If any other code in your business logic were as complicated and fragile as your multithreading code, you’d insist that it be rewritten. You should not make an exception for multithreading code. You can’t afford to.
I’ve found that the most effective way to write multithreading code is to avoid mutable data and use inter-thread communications protocols in much the same way that we use inter-process communications protocols when working with cooperating processes. I’ve developed the rule with which I opened this article:
If you write a lock statement you’re doing something wrong.
Programs containing threads that communicate by mutating shared state are fragile. Using locks to protect mutable data fields makes your program more complicated and raises other issues. A more complex program means that it’s harder to write, harder to understand, harder to modify, and more likely to contain intermittent deadlocks and race conditions that aren’t revealed until late in the project–often during user acceptance testing. Worse, proving the correctness of such code is incredibly difficult even for very experienced programmers.
The place for locks and such, if any, is in the data structures that your threads use to communicate with each other. In producer/consumer programs, for example, using a concurrent queue structure such as the .NET BlockingCollection completely eliminates the need for explicit locking in most applications. People who are a lot smarter than you, me, and the vast majority of other programmers have spent years developing and testing those data structures so that you and I can concentrate on our business logic rather than on the incredibly complex world of locks, semaphores, etc.
Programs that work with multiple threads are inherently more complex than single threaded programs. But they don’t have to be so monumentally more complex that even advanced programmers have difficulty doing the simplest things. I’ve found that I can eliminate shared mutable state by using concurrent collections and other inter-thread communications. And when I eliminate the shared mutable state I find there is no need in those programs for explicit synchronization.
I’ll be exploring that idea in future articles.