Agradecimientos
Me gustaría dar las gracias a mis dos artistas, Jeniffer Kohnke y Angela Brooks. Jennifer es la encargada de las impresionantes ilustraciones del inicio de cada capítulo y también de los retratos de Kent Beck, Ward Cunningham, Bjarne Stroustrup, Ron Jeffries, Grady Booch, Dave Thomas, Michael Feathers y el mío propio.
Angela se encarga de las ilustraciones internas de los capítulos. Ha realizado muchos dibujos para mí en los últimos años, incluidos muchos de los del libro Agile Software Development: Principles, Patterns, and Practices. También es mi primogénita.
Un agradecimiento especial a los revisores Bob Bogetti, George Bullock, Jeffrey Overbey y especialmente Matt Heusser. Han sido increíbles. Han sido inmisericordes. Han sido minuciosos. Me han forzado al máximo para realizar las mejoras necesarias.
Gracias a mi editor, Chris Guzikowski, por su apoyo, aliento y amistad. Gracias a todo el personal de la editorial, incluida Raina Chrobak, que se encargó de que fuera honesto y cumpliera los plazos.
Gracias a Micah Martin y a todos los de 8th Light (www.8thlight.com) por sus críticas y su apoyo.
Gracias a todos los Object Mentor, pasados, presentes y futuros, incluidos Bob Koss, Michael Feathers, Michael Hill, Erik Meade, Jeff Langr, Pascal Roy, David Farber, Brett Schuchert, Dean Wampler, Tim Ottinger, Dave Thomas, James Grenning, Brian Button, Ron Jeffries, Lowell Lindstrom, Angelique Martin, Cindy Sprague, Libby Ottinger, Joleen Craig, Janice Brown, Susan Rosso y el resto.
Gracias Jim Newkirk, mi amigo y socio, que me ha enseñado más de lo que cree. Mi agradecimiento a Kent Beck, Martin Fowler, Ward Cunningham, Bjarne Stroustrup, Grady Booch y todos mis mentores, compatriotas y colegas. Gracias a John Vlissides por estar ahí cuando lo necesitaba. Gracias a todos los de Zebra por permitirme despotricar sobre la extensión que debe tener una función.
Y, por último, darle las gracias por leer estos agradecimientos.
Apéndice A
Concurrencia II
por Brett L. Schuchert
Este apéndice complementa y amplía el capítulo 13 sobre concurrencia. Se ha escrito como una serie de temas independientes que puede leer en el orden que desee. Algunas secciones están duplicadas para facilitar dicha lectura.
Ejemplo cliente/servidor
Imagine una sencilla aplicación cliente/servidor. Un servidor espera a que un cliente se conecte. Un cliente se conecta y envía una solicitud.
El servidor
A continuación le mostramos una versión simplificada de una aplicación de servidor. El código completo de este ejemplo se recoge en el Listado A-3.
ServerSocket serverSocket = new ServerSocket(8009);
while (keepProcessing) {
try {
Socket socket = serverSocket.accept();
process(socket);
} catch (Exception e) {
handle(e);
}
}
Esta sencilla aplicación espera una conexión, procesa un mensaje entrante y vuelve a esperar a la siguiente solicitud cliente. El código cliente para conectarse al servidor es el siguiente:
private void connectSendReceive(int i) {
try {
Socket socket = new Socket (“localhost”, PORT);
MessageUtils.sendMessage(socket, Integer.toString(i));
MessageUtils.getMessage(socket);
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
¿Cómo se comporta esta combinación de cliente y servidor? ¿Cómo podemos describir formalmente ese rendimiento? La siguiente prueba afirma que el rendimiento es aceptable:
@Test(timeout = 10000)
public void shouldRunInUnder10Seconds() throws Exception {
Thread[] threads = createThreads();
startAllThreadsw(threads);
waitForAllThreadsToFinish(threads);
}
Se omite la configuración para que el ejemplo sea sencillo (véase “ ClientTest.java ” más adelante). Esta prueba afirma que debe completarse en 10 000 milisegundos.
Es un ejemplo clásico de validación del rendimiento de un sistema. Este sistema debe completar una serie de solicitudes cliente en 10 segundos. Mientras el servidor pueda procesar cada solicitud cliente a tiempo, la prueba será satisfactoria.
¿Qué sucede sí la prueba falla? Aparte de desarrollar algún tipo de bucle de consulta de eventos, no hay mucho que hacer en un único proceso para aumentar la velocidad de este código. ¿Se solucionaría el problema con varios procesos? Puede, pero necesitamos saber cómo se consume el tiempo. Hay dos posibilidades:
- E/S: Con un socket, conectándose a la base de datos, esperando al intercambio de memoria virtual, etc.
- Procesador Cálculos numéricos, procesamiento de expresiones regulares, recolección de elementos sin usar, etc.
Los sistemas suelen tener uno de cada, pero para una operación concreta suele haber uno dominante. Si el código se vincula al procesador, mayor cantidad de hardware de procesamiento puede mejorar el rendimiento y hacer que se supere la prueba, pero no hay tantos ciclos de CPU disponibles, de modo que añadir procesos a un problema vinculado al procesador no hará que aumente la velocidad.
Por otra parte, sí el proceso está vinculado a E/S, la concurrencia puede aumentar la eficacia. Cuando una parte del sistema espera a E/S, otra puede usar ese tiempo de espera para procesar algo distinto, maximizando el uso eficaz de la CPU disponible.
Añadir subprocesos
Imagine que la prueba de rendimiento falla. ¿Cómo podemos mejorar la producción para que la prueba de rendimiento sea satisfactoria? Si el método process del servidor está vinculado a la E/S, existe una forma de conseguir que el servidor use subprocesos (basta con cambiar processMessage):
void process (final Socket socket) {
if (socket == null)
return;
Runnable clientHandler = new Runnable() {
public void run() {
try {
String message = MessageUtils.getMessage(socket);
MessageUtils.sendMessage(socket, “Processed: ” + message);
closeIgnoringException(socket);
} catch (Exception e) {
e.printStackTrace();
}
}
};
Thread clientConnection = new Thread(clientHandler);
clientConnection.start();
}
Asuma que este cambio hace que la prueba se supere; el código es completo, ¿correcto?
Observaciones del servidor
El servidor actualizado completa satisfactoriamente la prueba en algo más de un segundo. Desafortunadamente, la solución genera ciertos problemas.
¿Cuántos subprocesos podría crear nuestro servidor? El código no define límites de modo que podríamos alcanzar el impuesto por la Máquina virtual de Java (MVJ), suficiente en muchos sistemas sencillos. ¿Pero sí el sistema tiene que asumir multitud de usuarios de una red pública? Si se conectan demasiados usuarios al mismo tiempo, el sistema podría colapsarse.
Pero dejemos temporalmente este problema de comportamiento, La solución mostrada tiene problemas de limpieza y estructura. ¿Cuántas responsabilidades tiene el código del servidor?
- Administración de conexiones.
- Procesamiento de clientes.
- Política de subprocesos.
- Política de cierre del servidor.
Desafortunadamente, todas estas responsabilidades se encuentran en la función process . Además, el código cruza varios niveles diferentes de abstracción. Por tanto, a pesar de la reducida función process , es necesario dividirlo.
Existen varios motivos para cambiar el servidor; por tanto, incumple el principio de responsabilidad única. Para mantener la limpieza de un sistema concurrente, la administración de subprocesos debe limitarse a una serie de puntos controlados. Es más, el código que gestione los subprocesos únicamente debe encargarse de la gestión de subprocesos. ¿Por qué? Si no existe otro motivo, el control de problemas de concurrencia ya es lo suficientemente complicado como para generar simultáneamente otros problemas no relacionados con la concurrencia.