Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Advanced CORBA Programming wit C++ - M. Henning, S. Vinoski.pdf
Скачиваний:
65
Добавлен:
24.05.2014
Размер:
5 Mб
Скачать

IT-SC book: Advanced CORBA® Programming with C++

Part VI: Power CORBA

Chapter 21. Multithreaded Applications

21.1 Chapter Overview

In this chapter we explore issues related to multithreaded CORBA applications. Section 21.3 explains the benefits that multithreading brings to CORBA applications. Sections

21.4 and 21.5 discuss fundamental multithreading techniques and explain how the ORB and POA help support them. In Section 21.6 we convert the servant locator example first presented in Chapter 12 to work properly in the presence of multiple

threads. Finally, in Section 21.7 we briefly discuss multithreading problems related to servant activators.

21.2 Introduction

Practical CORBA applications must be able to scale well in several dimensions. These dimensions include the number of objects that an application can support, the number of requests it can handle simultaneously, the number of connections it allows, and the amount of CPU and memory resources it uses.

One important method of making applications scale well is to employ multithreaded programming techniques. Although multiple threads allow true concurrent programming only on multi-CPU machines, using them can simplify program logic as well as enhance program scalability and performance.

This chapter provides a high-level overview of how multithreaded programming techniques can be used to develop CORBA applications. We do not intend to provide an in-depth tutorial on threads because doing this properly would itself require a book. Fortunately, a number of good books and articles on multithreaded programming and concurrency for distributed applications exist [2] [10] [12>] [35] [36] [37], and we recommend that you read and study them if you need to brush up on your multithreaded programming skills.

21.3 Motivation for Multithreaded Programs

Ordinarily, processes on commonly used operating systems such as Windows NT and various flavors of UNIX are single-threaded. All actions taken by a single-threaded process, from accessing a variable on the run-time stack to sending and receiving network packets through a socket, are performed by the single thread of control that runs within the process.

827

IT-SC book: Advanced CORBA® Programming with C++

Unfortunately, it is often difficult to develop and use server applications that use only a single thread of control. The following sections explain why.

21.3.1 Request Serialization

When only a single thread is available, the server must serialize the processing of client requests. New requests arriving at the server are queued in the POA that hosts the target object. As described in Chapter 11, a POA with the SINGLE_THREAD_MODEL value for its ThreadPolicy must be capable of queuing incoming requests while an object is already busy processing a request.

If processing any request takes a long time, it prevents the processing of all other requests for objects in the same POA. It is thus possible for the request queue in the POA to grow too large. If this occurs, the POA raises a TRANSIENT exception back to the client to tell it to retry the request, with the hope that the number of queued requests will have been reduced by the time the retry is received.

21.3.2 Event Handling

Because server applications wait for requests to arrive on their advertised network ports, they are often described as reactive [34]. To detect when requests arrive, most ORB implementations employ an event loop to monitor their network connections. For example, ORB implementations often use the UNIX select system call to watch for events occurring on the set of file descriptors corresponding to the server's network connections. When a request arrives, the ORB reacts by dispatching the request to the application so that it can be carried out.

For event handling to occur properly, the ORB must be able to gain the thread of control from time to time. Unfortunately, if a long-running request ties up the single thread doing other things, such as performing a complicated calculation, it denies the ORB the ability to wait for requests; the server cannot read incoming messages from its network connections. When this occurs, it can cause the network transport to apply flow control to make the clients' network transports stop sending messages. If each client ORB continues to attempt to send requests under these conditions, it will start getting errors from its own network transport layer and thus each client also may have to buffer requests. Thus, a single long-running request can deny service to numerous clients.

One way to prevent these conditions is to ensure that no requests take a long time to complete. If the nature of a request is such that it will take a while to process, the request can be broken up in two ways.

Break the IDL operation into two parts: one to start the request and the other to obtain the results. For example, assume that an interface has one operation that we know will take a long time to execute.

interface Rocket {

828

IT-SC book: Advanced CORBA® Programming with C++

typedef sequence<octet> Telemetry;

Telemetry get_all_telemetry();

};

If the target Rocket object is being flown only for a few minutes, it might be practical to have the implementation of get_all_telemetry simply wait for its flight to complete and then return all the telemetry data in one chunk. If the rocket is heading for the moon, however, this approach is clearly not practical.

Breaking the operation into two parts might yield an interface that looks like this:

interface Rocket {

typedef sequence<octet> Telemetry;

void start_gathering_telemetry();

Telemetry get_telemetry(out boolean no_more_data);

};

We first invoke start_gathering_telemetry to tell the target Rocket that we intend to start requesting telemetry data. When we want data we invoke get_telemetry, which returns the data if there are any and sets the no_more_data argument to true if telemetry collection has completed.

This approach lets the implementation of the start_gathering_telemetry operation set a flag or create a work item that indicates that telemetry should be collected; then it immediately returns. When our client invokes the get_telemetry method, its implementation can return whatever data have been collected to that point, setting no_more_data appropriately. The server application can thus switch between gathering telemetry and allowing the ORB to listen for requests (by invoking ORB::perform_work as described in Section 11.11.2) without allowing either activity to block the other.

Break the interface into two parts instead of splitting the operation:

typedef sequence<octet> Telemetry;

interface TelemetrySubscriber {

void new_telemetry(in Telemetry data);

void telemetry_complete();

};

interface Rocket {

void start_gathering_telemetry(

in TelemetrySubscriber subscriber

);

};

829

IT-SC book: Advanced CORBA® Programming with C++

This approach, usually referred to as publish/subscribe, relies on the server publishing information in a callback to the subscribing client. A client that wants to receive telemetry implements an object that supports the TelemetrySubscriber interface and passes its reference to the start_gathering_telemetry operation. The server then calls back to the TelemetrySubscriber object's new_telemetry operation in the client whenever it has data to send. When there is no more telemetry, the server informs the client by invoking telemetry_complete on the TelemetrySubscriber object.

Unfortunately, these two approaches share a common problem: the implementation of the system dictates its interface. Our implementation of the telemetry-fetching operation suffers from problems because it is single-threaded and takes a long time to complete, but that should not force us to redesign our interfaces to accommodate it.

Another problem with our redesigned interfaces is that they rely on callbacks. Distributed callbacks are fraught with problems, as we describe in Section 20.3. Furthermore, unless a single-threaded ORB is careful to use non-blocking I/O when waiting for replies, distributed callbacks can easily cause deadlock. For example, suppose that a singlethreaded client sends a request to a server and then blocks reading its network port waiting for the reply. If the server attempts to call back to the client, deadlock will occur. That's because the server is trying to talk to the client to carry out the original request, but the client is busy waiting for the server to reply to the original request. Each one is preventing the other from proceeding. It is possible to design single-threaded ORBs to avoid this kind of deadlock, but you should be aware that not all ORBs provide this capability.

21.3.3 Evaluating Single-Threaded Servers

Redesigning our telemetry retrieval interfaces to avoid problems due to single-threaded operation indicates that we have a problem. Specifically, it indicates the application convolution that results from attempting to write reactive systems without multiple threads. As the complexity of the application increases, it becomes more and more difficult to ensure that all tasks in the system are getting their share of the single thread. Artificial boundaries begin to appear where one task explicitly yields the single thread to other tasks. Preventing task starvation becomes more and more difficult, and maintenance becomes complicated because each modification of the program requires analysis to ensure that it does not introduce task starvation.

All in all, single-threaded operation is fine for servers that are used by only a few clients for short-duration requests. Pure client applications, which contain no CORBA objects, also work well when single-threaded, especially when they perform only synchronous request invocations. High-performance servers, on the other hand, are usually multithreaded.

21.3.4 Benefits of Multithreaded Programming

830