The problem with state

Một phần của tài liệu Manning clojure in action 2nd (Trang 159 - 162)

State is the current set of values associated with things in a program. For example, a payroll program might deal with employee objects. Each employee object represents the state of the employee, and every program usually has a lot of such state. There’s no problem with state, per se, or even with mutating state. The real world is full of per- ceived changes: people change, plans change, the weather changes, and the balance in a bank account changes. The problem occurs when concurrent (multithreaded) programs share this sort of state among different threads and then attempt to make updates to it. When the illusion of single-threaded execution breaks down, the code encounters all manner of inconsistent data. In this section, we’ll look at a solution to this problem. But before we do, let’s recap the issues faced by concurrent programs operating on shared data.

6.1.1 Common problems with shared state

Most problems with multithreaded programs happen because changes to shared data aren’t correctly protected. For the purposes of this chapter, we’ll summarize the issues as follows.

LOSTORBURIEDUPDATES

Lost updates occur when two threads update the same data one after the other. The update made by the first thread is lost because the second one overwrites it. A classic example is two threads incrementing a counter, the current value of which is 10.

Because execution of threads is interleaved, both threads can do a read on the counter and think the value is 10, and then both increment it to 11. The problem is that the final value should have been 12, and the update done by one of the threads was lost.

DIRTYANDUNREPEATABLEREADS

A dirty read happens when a thread reads data that another thread is in the process of updating. Before the one thread could completely update the data, the other thread has read inconsistent (dirty) data. Similarly, an unrepeatable read happens when a thread reads a particular data set, but because other threads are updating it, the thread can never do another read that results in it seeing the same data again.

137 The problem with state

PHANTOMREADS

A phantom read happens when a thread reads data that’s been deleted (or more data is added). The reading thread is said to have performed a phantom read because it has summarily read data that no longer exists.

Brian Goetz’s book Java Concurrency in Practice (Addison-Wesley Professional, 2006) does an incredible job of throwing light on these issues. The book uses Java to illus- trate examples, so it isn’t directly useful, but it’s still highly recommended.

6.1.2 Traditional solution

The most obvious solution to these problems is to impose a level of control on those parts of the code that deal with such mutable, shared data. This is done using locks, which are constructs that control the execution of sections of code, ensuring that only a single thread runs a lock-protected section of code at a time. When using locks, a thread can execute a destructive method (one that mutates data) that’s protected with a lock only if it’s able to first obtain an associated lock. If a thread tries to execute such code while some other thread holds the lock, it blocks until the lock becomes avail- able again. The blocking thread is allowed to resume execution only after it obtains the lock at a later time.

This approach might seem reasonable, but it gets complicated the moment more than one piece of mutable data needs a coordinated change. When this happens, each thread that needs to make the change must obtain multiple locks, leading to more contention and resulting in concurrency problems. It’s difficult to ensure cor- rectness of multithreaded programs that have to deal with multiple mutating data structures. Further, finding and fixing bugs in such programs is difficult thanks to the inherently nondeterministic nature of multithreaded programs.

Still, programs of significant complexity have been written using locks. It takes a lot more time and money to ensure things work as expected and a larger mainte- nance budget to ensure things continue to work properly while changes are being made to the program. It makes you wonder if there isn’t a better approach to solving this problem.

This chapter is about such an approach. Before we get into the meat of the solu- tion, we’ll examine a couple of things. First, we’ll look at the general disadvantages of using locks in multithreaded programs. Then, we’ll take a quick overview of the new issues that arise from the presence of locking.

DISADVANTAGESOFLOCKING

The most obvious disadvantage of locking is that code is less multithreaded than it was before the introduction of locks. When one thread obtains and holds a lock, no other thread can execute that code, causing other threads to wait. This can be wasteful, and it reduces throughput of multithreaded applications.

Further, locks are an excessive solution. Consider the case where a thread only wants to read some piece of mutable data. To ensure that no other thread makes changes while it’s doing its work, the reader thread must lock all concerned mutable

data. This causes not only writers to block but other readers too. This is unnecessar- ily wasteful.

Lastly, another disadvantage of locking is that you, the programmer, must remember to lock, and lock the right things, and in the right order. If someone introduces a bug that involves a forgotten lock, it can be difficult to track down and fix. There are no automatic mechanisms to flag this situation and no compile-time or runtime warnings associated with such situations, other than the fact that the program behaves in an unexpected manner! The knowledge of what to lock and in what order to lock things (so that the locks can be released in the reverse order) can’t be expressed within pro- gram code—typically, it’s recorded in technical documentation. Everyone in the soft- ware industry knows how well documentation works.

Unfortunately, these aren’t the only disadvantages of using locking; it causes new problems too. We’ll examine some of them now.

NEWPROBLEMSWITHLOCKING

When a single thread needs to change more than one piece of mutable data, it needs to obtain locks for all of them. This is the only way for a lock-based solution to ensure coordinated changes to multiple items. The fact that threads need to obtain locks to do their work causes contention for these locks. This contention results in a few issues that are typically categorized as shown in table 6.1.

With all these disadvantages and issues that accompany the use of locks, you must wonder if there isn’t a better solution to the problem of concurrency and state. We’ll explore this in the next section, beginning with a fresh look at modeling state itself.

Table 6.1 Issues that arise from the use of locks

Issue Description

Deadlock This is the case where two or more threads wait for the other to release locks that they need. This cyclic dependency results in all concerned threads being unable to proceed.

Starvation This happens when a thread isn’t allocated enough resources to do its job, causing it to starve and never complete.

Livelock This is a special case of starvation, and it happens when two threads continue executing (that is, changing their states) but make no progress toward their final goal. Imagine two people meeting in a hallway and each trying to pass the other. If they both wait for the other to move, it results in a deadlock. If they both keep moving toward the other, they still end up blocking each other from passing. This situation results in a livelock, because they’re both doing work and changing states but are still unable to proceed.

Race condition

This is a general situation where the interleaving of execution of threads causes an unde- sired computational result. Such bugs are difficult to debug because race conditions hap- pen in relatively rare scenarios.

139 Separating identities and values

Một phần của tài liệu Manning clojure in action 2nd (Trang 159 - 162)

Tải bản đầy đủ (PDF)

(338 trang)