What follows is a series of principles and techniques for defending your systems from the
problems of concurrent code.
Recommendation: Attempt to partition data into independent subsets than can be
operated on by independent threads , possibly in different processors.
Starvation:
One thread or a group of threads is prohibited from proceeding
for an excessively long time or forever. For example, always letting
fast-running threads through first could starve out longer running
threads if there is no end to the fast-running threads.
Bound Resources, Mutual Exclusion, Starvation, Deadlock, Livelock
Given these definitions, we can now discuss the various execution models used in
concurrent programming.
In all cases the code under test was known to be incorrect. This just reinforced the fact
that different operating systems have different threading policies , each of which impacts
the code’s execution .
The reason that threading bugs can be infrequent , sporadic , and hard to repeat, is that
only a very few pathways out of the many thousands of possible pathways through a vulnerable
section actually fail. So the probability that a failing pathway is taken can be startlingly
low . This makes detection and debugging very difficult.
How might you increase your chances of catching such rare occurrences?
Each of these methods can affect the order of execution, thereby increasing the odds
of detecting a flaw . It’s better when broken code fails as early and as often as possible .
Did you know that the threading model in Java does not guarantee preemptive threading ? Modern OS’s support preemptive threading, so you get that “for free.” Even so , it not guaranteed by the JVM.
If you run your tests a thousand times with random jiggling, you may root out some flaws . If the tests pass, at least you can say you’ve done due diligence . Though a bit simplistic , this could be a reasonable option in lieu of a more sophisticated tool.
Use jiggling strategies to ferret out errors.
Concurrent code is difficult to get right . Code that is simple to follow can become nightmarish
when multiple threads and shared data get into the mix . If you are faced with writing
concurrent code, you need to write clean code with rigor or else face subtle and
infrequent failures .
Issues will crop up . The ones that do not crop up early are often written off as a onetime
occurrence . These so-called one-offs typically only happen under load or at seemingly
random times. Therefore, you need to be able to run your thread-related code in
many configurations on many platforms repeatedly and continuously .
If we have learned anything over the last couple of decades ,
it is that programming is a craft more than it is a science . To write clean code, you must
first write dirty code and then clean it.
This should not be a surprise to you. We learned this truth in grade school when our
teachers tried (usually in vain ) to get us to write rough drafts of our compositions. The
process, they told us, was that we should write a rough draft, then a second draft, then several
subsequent drafts until we had our final version. Writing clean compositions, they
tried to tell us, is a matter of successive refinement .
Most freshman programmers (like most grade-schoolers ) don’t follow this advice particularly
well. They believe that the primary goal is to get the program working. Once it’s
“working,” they move on to the next task, leaving the “working” program in whatever state
they finally got it to “work.” Most seasoned programmers know that this is professional
suicide .
With all the marshalling moved to the ArgumentMarshaler, I started pushing functionality
into the derivatives .The first step was to move the setBoolean function into the
BooleanArgumentMarshaller and make sure it got called correctly . So I created an abstract
set method.
If you look closely at setIntArg, you'll notice that it uses two instance variables: args
and currentArg. To move setIntArg down into BooleanArgumentMarshaler, I'll have to pass
both args and currentArgs as function arguments. That's dirty [F1]. I'd rather pass one
argument instead of two. Fortunately, there is a simple solution. We can convert the args
array into a list and pass an Iterator down to the set functions. The following took me
ten steps , passing all the tests after each. But I'll just show you the result. You should be
able to figure out what most of the tiny little steps were.
Much of good software design is simply about partitioning —creating appropriate
places to put different kinds of code. This separation of concerns makes the code much
simpler to understand and maintain.
Frankly, it’s a compromise . Users who don’t like the error messages supplied by
ArgsException will have to write their own. But the convenience of having canned error
messages already prepared for you is not insignificant.
By now it should be clear that we are within striking distance of the final solution that
appeared at the start of this chapter. I’ll leave the final transformations to you as an exercise.
It is not enough for code to work. Code that works is often badly broken. Programmers
who satisfy themselves with merely working code are behaving unprofessionally. They
may fear that they don’t have time to improve the structure and design of their code, but I
disagree. Nothing has a more profound and long-term degrading effect upon a development
project than bad code. Bad schedules can be redone, bad requirements can be redefined.
Bad team dynamics can be repaired. But bad code rots and ferments, becoming an
inexorable weight that drags the team down. Time and time again I have seen teams grind
to a crawl because, in their haste, they created a malignant morass of code that forever
thereafter dominated their destiny.
Of course bad code can be cleaned up. But it’s very expensive. As code rots, the modules
insinuate themselves into each other, creating lots of hidden and tangled dependencies .
Finding and breaking old dependencies is a long and arduous task. On the other hand,
keeping code clean is relatively easy. If you made a mess in a module in the morning, it is
easy to clean it up in the afternoon. Better yet , if you made a mess five minutes ago, it’s
very easy to clean it up right now.
So the solution is to continuously keep your code as clean and simple as it can be.
Never let the rot get started.
JUnit has had many authors, but it began with Kent Beck and Eric Gamma together on a
plane to Atlanta. Kent wanted to learn Java, and Eric wanted to learn about Kent’s Smalltalk
testing framework. “What could be more natural to a couple of geeks in cramped
quarters than to pull out our laptops and start coding? ” After three hours of high-altitude
work, they had written the basics of JUnit.
Notice that this required us to promote compactExpected and compactActual to member
variables . I don’t like the way that the last two lines of the new function return variables,
but the first two don’t. They aren’t using consistent conventions [G11] . So we should
change findCommonPrefix and findCommonSuffix to return the prefix and suffix values.
I’m not really happy with this. The passing of the prefixIndex argument is a bit arbitrary
[G32]. It works to establish the ordering but does nothing to explain the need for that
ordering. Another programmer might undo what we have done because there’s no indication
that the parameter is really needed. So let’s take a different tack .
There is a problem however . As I was eliminating the +1s, I noticed the following line
in compactString.
This is much better! Now we see that the compactString function is simply composing the
fragments together . We can probably make this even clearer. Indeed, there are lots of little
cleanups we could do . But rather than drag you through the rest of the changes , I’ll just
show you the result in Listing 15-5.
This is actually quite pretty . The module is separated into a group of analysis functions
and another group of synthesis functions . They are topologically sorted so that the
definition of each function appears just after it is used. All the analysis functions appear
first, and all the synthesis functions appear last.
Often one refactoring leads to another that leads to the undoing of the
first. Refactoring is an iterative process full of trial and error , inevitably converging on
something that we feel is worthy of a professional.
And so we have satisfied the Boy Scout Rule . We have left this module a bit cleaner than
we found it. Not that it wasn’t clean already . The authors had done an excellent job with it .
But no module is immune from improvement , and each of us has the responsibility to
leave the code a little better than we found it.