Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

modern-multithreading-c-java

.pdf
Скачиваний:
18
Добавлен:
22.05.2015
Размер:
4.77 Mб
Скачать

CONTENTS

ix

5.4Message-Based Solutions to Concurrent Programming Problems, 275

5.4.1Readers and Writers, 275

5.4.2Resource Allocation, 278

5.4.3Simulating Counting Semaphores, 281

5.5Tracing, Testing, and Replay for Message-Passing Programs, 281

5.5.1SR-Sequences, 282

5.5.2Simple SR-Sequences, 288

5.5.3Determining the Feasibility of an SR-Sequence, 290

5.5.4Deterministic Testing, 296

5.5.5Reachability Testing for Message-Passing Programs, 297

5.5.6Putting It All Together, 299

Further Reading, 304

References, 304

Exercises, 304

6 Message Passing in Distributed Programs

312

6.1TCP Sockets, 312

6.1.1Channel Reliability, 313

6.1.2TCP Sockets in Java, 314

6.2Java TCP Channel Classes, 317

6.2.1Classes TCPSender and TCPMailbox, 318

6.2.2Classes TCPSynchronousSender and TCPSynchronousMailbox, 326

6.2.3Class TCPSelectableSynchronousMailbox, 328

6.3Timestamps and Event Ordering, 329

6.3.1Event-Ordering Problems, 330

6.3.2Local Real-Time Clocks, 331

6.3.3Global Real-Time Clocks, 332

6.3.4Causality, 332

6.3.5Integer Timestamps, 334

6.3.6Vector Timestamps, 335

6.3.7Timestamps for Programs Using Messages and Shared Variables, 339

6.4Message-Based Solutions to Distributed Programming Problems, 341

6.4.1Distributed Mutual Exclusion, 341

6.4.2Distributed Readers and Writers, 346

6.4.3Alternating Bit Protocol, 348

6.5Testing and Debugging Distributed Programs, 353

6.5.1Object-Based Sequences, 353

6.5.2Simple Sequences, 362

x

CONTENTS

6.5.3Tracing, Testing, and Replaying CARC-Sequences and CSC-Sequences, 362

6.5.4Putting It All Together, 369

6.5.5Other Approaches to Replaying Distributed

Programs, 371

Further Reading, 374

References, 375

Exercises, 376

7 Testing and Debugging Concurrent Programs

381

7.1Synchronization Sequences of Concurrent Programs, 383

7.1.1Complete Events vs. Simple Events, 383

7.1.2Total Ordering vs. Partial Ordering, 386

7.2Paths of Concurrent Programs, 388

7.2.1Defining a Path, 388

7.2.2Path-Based Testing and Coverage Criteria, 391

7.3Definitions of Correctness and Faults for Concurrent Programs, 395

7.3.1Defining Correctness for Concurrent Programs, 395

7.3.2Failures and Faults in Concurrent Programs, 397

7.3.3Deadlock, Livelock, and Starvation, 400

7.4Approaches to Testing Concurrent Programs, 408

7.4.1Nondeterministic Testing, 409

7.4.2Deterministic Testing, 410

7.4.3Combinations of Deterministic and Nondeterministic Testing, 414

7.5Reachability Testing, 419

7.5.1Reachability Testing Process, 420

7.5.2SYN-Sequences for Reachability Testing, 424

7.5.3Race Analysis of SYN-Sequences, 429

7.5.4Timestamp Assignment, 433

7.5.5Computing Race Variants, 439

7.5.6Reachability Testing Algorithm, 441

7.5.7Research Directions, 447

Further Reading, 449

References, 449

Exercises, 452

Index

457

PREFACE

This is a textbook on multithreaded programming. The objective of this book is to teach students about languages and libraries for multithreaded programming, to help students develop problem-solving and programming skills, and to describe and demonstrate various testing and debugging techniques that have been developed for multithreaded programs over the past 20 years. It covers threads, semaphores, locks, monitors, message passing, and the relevant parts of Java, the POSIX Pthreads library, and the Windows Win32 Application Programming Interface (API).

The book is unique in that it provides in-depth coverage on testing and debugging multithreaded programs, a topic that typically receives little attention. The title Modern Multithreading reflects the fact that there are effective and relatively new testing and debugging techniques for multithreaded programs. The material in this book was developed in concurrent programming courses that the authors have taught for 20 years. This material includes results from the authors’ research in concurrent programming, emphasizing tools and techniques that are of practical use. A class library has been implemented to provide working examples of all the material that is covered.

Classroom Use

In our experience, students have a hard time learning to write concurrent programs. If they manage to get their programs to run, they usually encounter deadlocks and other intermittent failures, and soon discover how difficult it is to reproduce the failures and locate the cause of the problem. Essentially, they have no way to check the correctness of their programs, which interferes with learning. Instructors face the same problem when grading multithreaded programs. It

xi

xii

PREFACE

is tedious, time consuming, and often impossible to assess student programs by hand. The class libraries that we have developed, and the testing techniques they support, can be used to assess student programs. When we assign programming problems in our courses, we also provide test cases that the students must use to assess the correctness of their programs. This is very helpful for the students and the instructors.

This book is designed for upper-level undergraduates and graduate students in computer science. It can be used as a main text in a concurrent programming course or could be used as a supplementary text for an operating systems course or a software engineering course. Since the text emphasizes practical material, provides working code, and addresses testing and debugging problems that receive little or no attention in many other books, we believe that it will also be helpful to programmers in industry.

The text assumes that students have the following background:

žProgramming experience as typically gained in CS 1 and CS 2 courses.

žKnowledge of elementary data structures as learned in a CS 2 course.

žAn understanding of Java fundamentals. Students should be familiar with object-oriented programming in Java, but no “advanced” knowledge is necessary.

žAn understanding of C++ fundamentals. We use only the basic objectoriented programming features of C++.

žA prior course on operating systems is helpful but not required.

We have made an effort to minimize the differences between our Java and C++ programs. We use object-oriented features that are common to both languages, and the class library has been implemented in both languages. Although we don’t illustrate every example in both Java and C++, the differences are very minor and it is easy to translate program examples from one language to the other.

Content

The book has seven chapters. Chapter 1 defines operating systems terms such as process, thread, and context switch. It then shows how to create threads, first in Java and then in C++ using both the POSIX Pthreads library and the Win32 API. A C++ Thread class is provided to hide the details of thread creation in Pthreads/Win32. C++ programs that use the Thread class look remarkably similar to multithreaded Java programs. Fundamental concepts, such as atomicity and nondeterminism, are described using simple program examples. Chapter 1 ends by listing the issues and problems that arise when testing and debugging multithreaded programs. To illustrate the interesting things to come, we present a simple multithreaded C++ program that is capable of tracing and replaying its own executions.

Chapter 2 introduces concurrent programming by describing various solutions to the critical section problem. This problem is easy to understand but hard

PREFACE

xiii

to solve. The advantage of focusing on this problem is that it can be solved without introducing complicated new programming constructs. Students gain a quick appreciation for the programming skills that they need to acquire. Chapter 2 also demonstrates how to trace and replay Peterson’s solution to the critical section problem, which offers a straightforward introduction to several testing and debugging issues. The synchronization library implements the various techniques that are described.

Chapters 3, 4, and 5 cover semaphores, monitors and message passing, respectively. Each chapter describes one of these constructs and shows how to use it to solve programming problems. Semaphore and Lock classes for Java and C++/Win32/Pthreads are presented in Chapter 3. Chapter 4 presents monitor classes for Java and C++/Win32/Pthreads. Chapter 5 presents mailbox classes with send/receive methods and a selective wait statement. These chapters also cover the built-in support that Win32 and Pthreads provide for these constructs, as well as the support provided by J2SE 5.0 (Java 2 Platform, Standard Edition 5.0). Each chapter addresses a particular testing or debugging problem and shows how to solve it. The synchronization library implements the testing and debugging techniques so that students can apply them to their own programs.

Chapter 6 covers message passing in a distributed environment. It presents several Java mailbox classes that hide the details of TCP message passing and shows how to solve several distributed programming problems in Java. It also shows how to test and debug programs in a distributed environment (e.g., accurately tracing program executions by using vector timestamps). This chapter by no means provides complete coverage of distributed programming. Rather, it is meant to introduce students to the difficulty of distributed programming and to show them that the testing and debugging techniques presented in earlier chapters can be extended to work in a distributed environment. The synchronization library implements the various techniques.

Chapter 7 covers concepts that are fundamental to testing and debugging concurrent programs. It defines important terms, presents several test coverage criteria for concurrent programs, and describes the various approaches to testing concurrent programs. This chapter organizes and summarizes the testing and debugging material that is presented in depth in Chapters 2 to 6. This organization provides two paths through the text. Instructors can cover the testing and debugging material in the last sections of Chapters 2 to 6 as they go through those chapters, or they can cover those sections when they cover Chapter 7. Chapter 7 also discusses reachability testing, which offers a bridge between testing and verification, and is implemented in the synchronization library.

Each chapter has exercises at the end. Some of the exercises explore the concepts covered in the chapter, whereas others require a program to be written. In our courses we cover all the chapters and give six homework assignments, two in-class exams, and a project. We usually supplement the text with readings on model checking, process algebra, specification languages, and other research topics.

xiv

PREFACE

Online Resources

The home page for this book is located at

http://www.cs.gmu.edu/ rcarver/ModernMultithreading

This Web site contains the source code for all the listings in the text and for the synchronization libraries. It also contains startup files and test cases for some of the exercises. Solutions to the exercises are available for instructors, as is a copy of our lecture notes. There will also be an errata page.

Acknowledgments

The suggestions we received from the anonymous reviewers were very helpful. The National Science Foundation supported our research through grants CCR-8907807, CCR-9320992, CCR-9309043, and CCR-9804112. We thank our research assistants and the students in our courses at North Carolina State and George Mason University for helping us solve many interesting problems. We also thank Professor Jeff Lei at the University of Texas at Arlington for using early versions of this book in his courses.

My friend, colleague, and coauthor Professor K. C. Tai passed away before we could complete this book. K.C. was an outstanding teacher, a world-class researcher in the areas of software engineering, concurrent systems, programming languages, and compiler construction, and an impeccable and highly respected professional. If the reader finds this book helpful, it is a tribute to K.C.’s many contributions. Certainly, K.C. would have fixed the faults that I failed to find.

RICHARD H. CARVER

Fairfax, Virginia July 2005 rcarver@cs.gmu.edu

1

INTRODUCTION TO CONCURRENT PROGRAMMING

A concurrent program contains two or more threads that execute concurrently and work together to perform some task. In this chapter we begin with an operating system’s view of a concurrent program. The operating system manages the program’s use of hardware and software resources and allows the program’s threads to share the central processing units (CPUs). We then learn how to define and create threads in Java and also in C++ using the Windows Win32 API and the POSIX Pthreads library. Java provides a Thread class, so multithreaded Java programs are object-oriented. Win32 and Pthreads provide a set of function calls for creating and manipulating threads. We wrap a C++ Thread class around these functions so that we can write C++/Win32 and C++/Pthreads multithreaded programs that have the same object-oriented structure as Java programs.

All concurrent programs exhibit unpredictable behavior. This creates new challenges for programmers, especially those learning to write concurrent programs. In this chapter we learn the reason for this unpredictable behavior and examine the problems it causes during testing and debugging.

1.1 PROCESSES AND THREADS: AN OPERATING SYSTEM’S VIEW

When a program is executed, the operating system creates a process containing the code and data of the program and manages the process until the program terminates. User processes are created for user programs, and system processes

Modern Multithreading: Implementing, Testing, and Debugging Multithreaded Java and C++/Pthreads/Win32 Programs, By Richard H. Carver and Kuo-Chung Tai Copyright 2006 John Wiley & Sons, Inc.

1

2

INTRODUCTION TO CONCURRENT PROGRAMMING

are created for system programs. A user process has its own logical address space, separate from the space of other user processes and separate from the space (called the kernel space) of the system processes. This means that two processes may reference the same logical address, but this address will be mapped to different physical memory locations. Thus, processes do not share memory unless they make special arrangements with the operating system to do so.

Multiprocessing operating systems enable several programs to execute simultaneously. The operating system is responsible for allocating the computer’s resources among competing processes. These shared resources include memory, peripheral devices such as printers, and the CPU(s). The goal of a multiprocessing operating system is to have some process executing at all times in order to maximize CPU utilization.

Within a process, program execution entails initializing and maintaining a great deal of information [Anderson et al. 1989]. For instance:

žThe process state (e.g., ready, running, waiting, or stopped)

žThe program counter, which contains the address of the next instruction to be executed for this process

žSaved CPU register values

žMemory management information (page tables and swap files), file descriptors, and outstanding input/output (I/O) requests

The volume of this per-process information makes it expensive to create and manage processes.

A thread is a unit of control within a process. When a thread runs, it executes a function in the program. The process associated with a running program starts with one running thread, called the main thread, which executes the “main” function of the program. In a multithreaded program, the main thread creates other threads, which execute other functions. These other threads can create even more threads, and so on. Threads are created using constructs provided by the programming language or the functions provided by an application programming interface (API).

Each thread has its own stack of activation records and its own copy of the CPU registers, including the stack pointer and the program counter, which together describe the state of the thread’s execution. However, the threads in a multithreaded process share the data, code, resources, and address space of their process. The per-process state information listed above is also shared by the threads in the program, which greatly reduces the overhead involved in creating and managing threads. In Win32 a program can create multiple processes or multiple threads. Since thread creation in Win32 has lower overhead, we focus on single-process multithreaded Win32 programs.

The operating system must decide how to allocate the CPUs among the processes and threads in the system. In some systems, the operating system selects a process to run and the process selected chooses which of its threads will execute. Alternatively, the threads are scheduled directly by the operating system. At any

ADVANTAGES OF MULTITHREADING

3

given moment, multiple processes, each containing one or more threads, may be executing. However, some threads may not be ready for execution. For example, some threads may be waiting for an I/O request to complete. The scheduling policy determines which of the ready threads is selected for execution.

In general, each ready thread receives a time slice (called a quantum) of the CPU. If a thread decides to wait for something, it relinquishes the CPU voluntarily. Otherwise, when a hardware timer determines that a running thread’s quantum has completed, an interrupt occurs and the thread is preempted to allow another ready thread to run. If there are multiple CPUs, multiple threads can execute at the same time. On a computer with a single CPU, threads have the appearance of executing simultaneously, although they actually take turns running and they may not receive equal time. Hence, some threads may appear to run at a faster rate than others.

The scheduling policy may also consider a thread’s priority and the type of processing that the thread performs, giving some threads preference over others. We assume that the scheduling policy is fair, which means that every ready thread eventually gets a chance to execute. A concurrent program’s correctness should not depend on its threads being scheduled in a certain order.

Switching the CPU from one process or thread to another, known as a context switch, requires saving the state of the old process or thread and loading the state of the new one. Since there may be several hundred context switches per second, context switches can potentially add significant overhead to an execution.

1.2 ADVANTAGES OF MULTITHREADING

Multithreading allows a process to overlap I/O and computation. One thread can execute while another thread is waiting for an I/O operation to complete. Multithreading makes a GUI (graphical user interface) more responsive. The thread that handles GUI events, such as mouse clicks and button presses, can create additional threads to perform long-running tasks in response to the events. This allows the event handler thread to respond to more GUI events. Multithreading can speed up performance through parallelism. A program that makes full use of two processors may run in close to half the time. However, this level of speedup usually cannot be obtained, due to the communication overhead required for coordinating the threads (see Exercise 1.11).

Multithreading has some advantages over multiple processes. Threads require less overhead to manage than processes, and intraprocess thread communication is less expensive than interprocess communication. Multiprocess concurrent programs do have one advantage: Each process can execute on a different machine (in which case, each process is often a multithreaded program). This type of concurrent program is called a distributed program. Examples of distributed programs are file servers (e.g., NFS), file transfer clients and servers (e.g., FTP), remote log-in clients and servers (e.g., Telnet), groupware programs, and Web browsers and servers. The main disadvantage of concurrent programs is that they

4

INTRODUCTION TO CONCURRENT PROGRAMMING

are extremely difficult to develop. Concurrent programs often contain bugs that are notoriously difficult to find and fix. Once we have examined several concurrent programs, we’ll take a closer look at the special problems that arise when we test and debug them.

1.3 THREADS IN JAVA

A Java program has a main thread that executes the main() function. In addition, several system threads are started automatically whenever a Java program is executed. Thus, every Java program is a concurrent program, although the programmer may not be aware that multiple threads are running. Java provides a Thread class for defining user threads. One way to define a thread is to define a class that extends (i.e., inherits from) the Thread class. Class simpleThread in Listing 1.1 extends class Thread. Method run() contains the code that will be executed when a simpleThread is started. The default run() method inherited from class Thread is empty, so a new run() method must be defined in simpleThread in order for the thread to do something useful.

The main thread creates simpleThreads named thread1 and thread2 and starts them. (These threads continue to run after the main thread completes its statements.) Threads thread1 and thread2 each display a simple message and terminate. The integer IDs passed as arguments to the simpleThread constructor are used to distinguish between the two instances of simpleThread.

A second way to define a user thread in Java is to use the Runnable interface. Class simpleRunnable in Listing 1.2 implements the Runnable interface, which means that simpleRunnable must provide an implementation of method run(). The main method creates a Runnable instance r of class simpleRunnable, passes r as an argument to the Thread class constructor for thread3, and starts thread3.

Using a Runnable object to define the run() method offers one advantage over extending class Thread. Since class simpleRunnable implements interface Runnable, it is not required to extend class Thread, which means that

class simpleThread extends Thread {

public simpleThread(int ID) {myID = ID;}

public void run() {System.out.println(‘‘Thread ’’ + myID + ‘‘ is running.’’);} private int myID;

}

public class javaConcurrentProgram { public static void main(String[] args) {

simpleThread thread1 = new simpleThread(1); simpleThread thread2 = new simpleThread(2);

thread1.start(); thread2.start(); // causes the run() methods to execute

}

}

Listing 1.1 Simple concurrent Java program.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]