Chapter 13: Concurrency Get Your Nonthreaded Code Working Fir
This page is a generated reference surface for selective reading. It exists to keep the learner apps guide-first while still preserving source access.
Learning objectives
- Explain the main ideas and vocabulary in Concurrency Get Your Nonthreaded Code Working Fir.
- Work through the source examples for Concurrency Get Your Nonthreaded Code Working Fir without depending on raw chunk order.
- Use Concurrency Get Your Nonthreaded Code Working Fir as selective reference when learner modules point back to Clean Code.
Prerequisites
- Earlier prerequisite concepts leading into Chapter 13: Concurrency Get Your Nonthreaded Code Working Fir.
Module targets
module-03-clean-codemodule-05-applied-design-and-code-review
AI companion modes
- Explain simply
- Socratic tutor
- Quiz me
- Challenge my understanding
- Diagnose my confusion
- Generate extra practice
- Revision mode
- Connect forward / backward
Source-of-truth note
This unit is anchored to Clean Code and the source chapter "Chapter 13: Concurrency Get Your Nonthreaded Code Working Fir". Use external resources only to clarify, extend, or modernize details without replacing the chapter's conceptual spine.
External enrichment
No chapter-specific enrichment resources are curated yet. Add them in the unit manifest when a source clearly improves learning.
Source provenance
- Primary source:
Clean Code - Source chapter 13: Chapter 13: Concurrency Get Your Nonthreaded Code Working Fir
- Raw source file:
053-chapter-13-concurrency-get-your-nonthreaded-code-working-fir.md - Raw source file:
054-conclusion-to-chapter-14-successive-refinement.md
Merged source
Chapter 13 Concurrency Get Your Nonthreaded Code Working Fir
Chapter 13: Concurrency: Get Your Nonthreaded Code Working First to Automated
Get Your Nonthreaded Code Working First
This may seem obvious, but it doesn't hurt to reinforce it. Make sure code works outside of its use in threads. Generally, this means creating POJOs that are called by your threads. The POJOs are not thread aware, and can therefore be tested outside of the threaded environment. The more of your system you can place in such POJOs, the better. Recommendation: Do not try to chase down nonthreading bugs and threading bugs at the same time. Make sure your code works outside of threads.
Make Your Threaded Code Pluggable
Write the concurrency-supporting code such that it can be run in several configurations:
-
One thread, several threads, varied as it executes
-
Threaded code interacts with something that can be both real or a test double.
-
Execute with test doubles that run quickly, slowly, variable.
-
Configure tests so they can run for a number of iterations.
Recommendation: Make your thread-based code especially pluggable so that you can run it in various configurations.
Make Your Threaded Code Tunable
Getting the right balance of threads typically requires trial an error. Early on, find ways to time the performance of your system under different configurations. Allow the number of threads to be easily tuned. Consider allowing it to change while the system is running. Consider allowing self-tuning based on throughput and system utilization.
Run with More Threads Than Processors
Things happen when the system switches between tasks. To encourage task swapping, run with more threads than processors or cores. The more frequently your tasks swap, the more likely you'll encounter code that is missing a critical section or causes deadlock.
Run on Different Platforms
In the middle of 2007 we developed a course on concurrent programming. The course development ensued primarily under OS X. The class was presented using Windows XP running under a VM. Tests written to demonstrate failure conditions did not fail as frequently in an XP environment as they did running on OS X. 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. Multithreaded code behaves differently in different environments.16
You should run your tests in every potential deployment environment. Recommendation: Run your threaded code on all target platforms early and often.
Instrument Your Code to Try and Force Failures
It is normal for flaws in concurrent code to hide. Simple tests often don't expose them. Indeed, they often hide during normal processing. They might show up once every few hours, or days, or weeks! 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? You can instrument your code and force it to run in different orderings by adding calls to methods
like Object.wait(), Object.sleep(), Object.yield() and Object.priority().
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. There are two options for code instrumentation:
-
Hand-coded
-
Automated
- 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.
Hand-Coded
You can insert calls to wait(), sleep(), yield(), and priority() in your code by hand. It might be just the thing to do when you're testing a particularly thorny piece of code. Here is an example of doing just that:
public synchronized String nextUrlOrNull() {
if(hasNext()) {
String url = urlGenerator.next();
Thread.yield(); // inserted for testing.
updateHasNext();
return url;
}
return null;
}
The inserted call to yield() will change the execution pathways taken by the code and possibly cause the code to fail where it did not fail before. If the code does break, it was not because you added a call to yield().17 Rather, your code was broken and this simply made the failure evident. There are many problems with this approach:
-
You have to manually find appropriate places to do this.
-
How do you know where to put the call and what kind of call to use?
-
Leaving such code in a production environment unnecessarily slows the code down.
-
It's a shotgun approach. You may or may not find flaws. Indeed, the odds aren't with you.
What we need is a way to do this during testing but not in production. We also need to easily mix up configurations between different runs, which results in increased chances of finding errors in the aggregate. Clearly, if we divide our system up into POJOs that know nothing of threading and classes that control the threading, it will be easier to find appropriate places to instrument the code. Moreover, we could create many different test jigs that invoke the POJOs under different regimes of calls to sleep, yield, and so on.
Automated
You could use tools like an Aspect-Oriented Framework, CGLIB, or ASM to programmatically instrument your code. For example, you could use a class with a single method:
public class ThreadJigglePoint {
public static void jiggle() {
}
}
- This is not strictly the case. Since the JVM does not guarantee preemptive threading, a particular algorithm might always work on an OS that does not preempt threads. The reverse is also possible but for different reasons.
You can add calls to this in various places within your code:
public synchronized String nextUrlOrNull() {
if(hasNext()) {
ThreadJiglePoint.jiggle();
String url = urlGenerator.next();
ThreadJiglePoint.jiggle();
updateHasNext();
ThreadJiglePoint.jiggle();
return url;
}
return null;
}
Now you use a simple aspect that randomly selects among doing nothing, sleeping, or yielding. Or imagine that the ThreadJigglePoint class has two implementations. The first implements jiggle to do nothing and is used in production. The second generates a random number to choose between sleeping, yielding, or just falling through. 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. There is a tool called ConTest,18 developed by IBM that does something similar, but it does so with quite a bit more sophistication. The point is to jiggle the code so that threads run in different orderings at different times. The combination of well-written tests and jiggling can dramatically increase the chance finding errors. Recommendation: Use jiggling strategies to ferret out errors.
Conclusion To Chapter 14 Successive Refinement
Conclusion to Chapter 14: Successive Refinement
Conclusion
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. First and foremost, follow the Single Responsibility Principle. Break your system into POJOs that separate thread-aware code from thread-ignorant code. Make sure when you are testing your thread-aware code, you are only testing it and nothing else. This suggests that your thread-aware code should be small and focused. Know the possible sources of concurrency issues: multiple threads operating on shared data, or using a common resource pool. Boundary cases, such as shutting down cleanly or finishing the iteration of a loop, can be especially thorny.
Learn your library and know the fundamental algorithms. Understand how some of the features offered by the library support solving problems similar to the fundamental algorithms. Learn how to find regions of code that must be locked and lock them. Do not lock regions of code that do not need to be locked. Avoid calling one locked section from another. This requires a deep understanding of whether something is or is not shared. Keep the amount of shared objects and the scope of the sharing as narrow as possible. Change designs of the objects with shared data to accommodate clients rather than forcing clients to manage shared state. 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. Testability, which comes naturally from following the Three Laws of TDD, implies some level of plug-ability, which offers the support necessary to run code in a wider range of configurations. You will greatly improve your chances of finding erroneous code if you take the time to instrument your code. You can either do so by hand or using some kind of automated technology. Invest in this early. You want to be running your thread-based code as long as possible before you put it into production. If you take a clean approach, your chances of getting it right increase drastically.
Bibliography
[Lea99]: Concurrent Programming in Java: Design Principles and Patterns, 2d. ed., Doug Lea, Prentice Hall, 1999.
[PPP]: Agile Software Development: Principles, Patterns, and Practices, Robert C. Martin, Prentice Hall, 2002.
[PRAG]: The Pragmatic Programmer, Andrew Hunt, Dave Thomas, Addison-Wesley, 2000.
This page intentionally left blank
Chapter 14: Successive Refinement
Case Study of a Command-Line Argument Parser
This chapter is a case study in successive refinement. You will see a module that started well but did not scale. Then you will see how the module was refactored and cleaned. Most of us have had to parse command-line arguments from time to time. If we don't have a convenient utility, then we simply walk the array of strings that is passed into the main function. There are several good utilities available from various sources, but none of them do exactly what I want. So, of course, I decided to write my own. I call it: Args.
Args is very simple to use. You simply construct the Args class with the input arguments and a format string, and then query the Args instance for the values of the arguments. Consider the following simple example:
Listing 14-1
Simple use of Args
public static void main(String[] args) {
try {
Args arg = new Args("l,p#,d*", args);
boolean logging = arg.getBoolean('l');
int port = arg.getInt('p');
String directory = arg.getString('d');
executeApplication(logging, port, directory);
} catch (ArgsException e) {
System.out.printf("Argument error: %s\n", e.errorMessage());
}
}
You can see how simple this is. We just create an instance of the Args class with two parameters. The first parameter is the format, or schema, string: "l,p#,d*." It defines three command-line arguments. The first, -l, is a boolean argument. The second, -p, is an integer argument. The third, -d, is a string argument. The second parameter to the Args constructor is simply the array of command-line argument passed into main. If the constructor returns without throwing an ArgsException, then the incoming command-line was parsed, and the Args instance is ready to be queried. Methods like getBoolean, getInteger, and getString allow us to access the values of the arguments by their names. If there is a problem, either in the format string or in the command-line arguments themselves, an ArgsException will be thrown. A convenient description of what went wrong can be retrieved from the errorMessage method of the exception.