Mostrando entradas con la etiqueta programación. Mostrar todas las entradas
Mostrando entradas con la etiqueta programación. Mostrar todas las entradas

lunes, 9 de marzo de 2015

Notificaciones en Android (2da Parte)

image

En el artículo anterior comenzamos a abordar el tema de las notificaciones en Android. Hablamos de los Toast que son las notificaciones más simples que tiene Android. En este artículo vamos a hablar acerca de las notificaciones en la barra de estado. Este tipo de notificaciones son las que se muestran en el dispositivo cuando entra un SMS o cuando nos hacen una llamada perdida.

Estas notificaciones generalmente constan de un icono y un texto descriptivo del mensaje, al desplegar la barra de estado vamos a poder ver la notificación más descriptiva.

Pues bien vamos a hacer un ejemplo donde mostremos una notificación en la barra de estado.

Vamos a crear un botón que al presionarlo pondrá en la barra de estado la notificación. El diseño de nuestro ejemplo quedará de la siguiente manera:

image

Ahora vamos a entrar en el código. Lo primero que tenemos que hacer es capturar el evento clic de nuestro botón para esto en el método onCreate de nuestra Activity vamos a capturar el botón.
Button boton = (Button)findViewById(R.id.button);
Y luego vamos a definir el onclick del botón
boton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {

}
});
Y es en el método onClick donde vamos a poner todo el código para mostrar la notificación. 

Lo primero que vamos a hacer es obtener una referencia del servicio de notificaciones de Android. Para esto utilizaremos el método getSystemService como muestra el siguiente código.

//Obteniendo la referencia al servicio de notificaciones
String service = Context.NOTIFICATION_SERVICE;
NotificationManager notManager = (NotificationManager) getSystemService(service);

Luego vamos a configurar el aspecto de la notificación que vamos a mostrar para ello definimos el icono que vamos a mostrar, el texto y la hora y se lo pasamos al constructor de la notificación.

//Configuramos la notificación
int icono = android.R.drawable.stat_notify_error;
CharSequence texto = "Probando Notificacion";
long hora = System.currentTimeMillis();

Notification notificacion = new Notification(icono, texto, hora);


Y por último nos queda definir el texto y la descripción que se mostrará al desplegar la barra de notificaciones, así como la Activity que se mostrará cuando seleccionemos la notificación.

//Configuramos el Intent
CharSequence titulo = "Mensaje de Alerta";
CharSequence descripcion = "Ejemplo de notificación.";

Intent notIntent = new Intent(getApplicationContext(),
MainActivity.class);

PendingIntent contIntent = PendingIntent.getActivity(
getApplicationContext(), 0, notIntent, 0);

notificacion.setLatestEventInfo(
getApplicationContext(), titulo, descripcion, contIntent);


Como podemos ver en el código anterior lo primero que hacemos es preparar la información, es decir el título, la descripción y el intent que ejecutaremos cuando la notificación sea seleccionada. Luego creamos un PendingIntent y le atachamos el intent creado y luego con el método getApplicationContext de la notificación terminamos de preparar la misma.

Una vez configurados todos los parámetros necesarios de la notificación pues procedemos a mostrar la misma, esto lo hacemos de la siguiente forma:

//Mostrando notificación
notManager.notify(1, notificacion);

Con el objeto NotificationManager creado al inicio y mediante su método notify conseguimos lo que queremos, pasándole un id único de notificación y el objeto Notificación que preparamos con anterioridad.

{ Leer Más }


miércoles, 28 de enero de 2015

Menús en Android (1ra Parte)

image

En este artículo vamos a hablar sobre los menús en Android, un recurso importante para nuestras aplicaciones cuando queremos mostrar opciones secundarias o queremos simplificar espacio dentro de la vista. En Android existen tres tipos diferentes de menús:

· Menús principales: De estos decir que son los más habituales, aparece cuando se oprime el botón menú del dispositivo en la parte inferior del mismo.

· Submenús: Son usados para mostrar opciones secundarias y se pueden mostrar al pulsar una opción del menú principal.

· Menús Contextuales: Aparece cuando se hace una pulsación larga sobre algún elemento de la vista de la aplicación.

En este artículo hablaremos sobre el primer tipo de menú y dejaremos los otros tipos de menú para próximos artículos.

Los menús los podemos implementar de dos formas en nuestra aplicación, una mediante la definición en un fichero XML y la segunda mediante código. En este artículo abordaremos la primera por ser la que más utilizo por su rápida implementación.

Para entender el tema de los menús pues nada mejor que llevarlo a la práctica y para esto le pondremos unos menús a la app que comenzamos en los artículos anteriores.

Para diseñar un menú es necesario crear el fichero menú.xml en la carpeta res / menú. En nuestra app pondremos un menú con tres opciones, por lo que el diseño quedaría así:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/Menu1" android:title="Opción 1"
android:icon="@drawable/tag"></item>
<item android:id="@+id/Menu2" android:title="Opción 2"
android:icon="@drawable/filter"></item>
<item android:id="@+id/Menu3" android:title="Opción 3"
android:icon="@drawable/chart"></item>
</menu>

Este XML se compone de un elemento principal <menu> que contiene tantos elementos <item> como opciones queramos ponerle a nuestro menú. Destacar que los elementos <item> tiene propiedades como el id, el título y el icono. Podemos observar que nuestro XML tiene un detalle y es que los títulos están puestos directamente, algo que vimos que no era aconsejable, para esto creamos los Strings de las opciones y lo referenciamos en el XML por lo que quedaría así:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/MenuPrincipal"
android:title="@string/texto_opcion1_menu"
android:icon="@drawable/tag"></item>
<item android:id="@+id/Menu2"
android:title="@string/texto_opcion2_menu"
android:icon="@drawable/filter"></item>
<item android:id="@+id/Menu3"
android:title="@string/texto_opcion3_menu"
android:icon="@drawable/chart"></item>
</menu>

Ahora tenemos que implementar el evento onCreateOptionsMenu() del activity que queremos que muestre el menú. En este evento vamos a “inflar” el menú. Primero obtendremos una referencia al inflater mediante el método getMenuInflater() y posteriormente generamos la estructura del menú llamando a su método inflate() pasándole como parámetro el ID del menú definido en el XML que en nuestro caso será R.menu.menu. Por último devolveremos el valor true para confirmar que debe mostrarse el menú.

Ahora solos nos queda implementar cada una de las opciones del menú. Esto se hará incluyendo el evento onOptionsItemSelected() del activity que mostrará el menú. Este evento recibe como parámetro el ítem que fue pulsado, cuyo ID podemos recuperar con el método getItemId(). Con el ID podemos saber qué opción ha sido pulsada y ejecutar lo que queramos. Por ahora lo único que haremos es mostrar un mensaje.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.Menu1:
Toast.makeText(this, "Opcion 1 pulsada!",
Toast.LENGTH_LONG).show();
return true;
case R.id.Menu2:
Toast.makeText(this, "Opcion 2 pulsada!",
Toast.LENGTH_LONG).show();
return true;
case R.id.Menu3:
Toast.makeText(this, "Opcion 3 pulsada!",
Toast.LENGTH_LONG).show();
return true;
default:
return super.onOptionsItemSelected(item);
}
}

De este código solo comentar como se muestra el mensaje y es utilizando la clase Toast, el mismo tiene el método makeText para indicar el texto que se mostrará en el mensaje y luego usamos el método show() para que se muestre en nuestro activity. De las notificaciones hablaremos en próximos artículos.

{ Leer Más }


lunes, 19 de enero de 2015

Nuestra primera aplicación (2da parte)

image

En el artículo anterior comenzamos a desarrollar una pequeña aplicación. Definimos toda la parte visual y en esta entrega vamos a desarrollar toda la lógica de la aplicación. Recordando, nuestra app lo único que hará es que al hacer clic en un botón, este cambie el valor de su texto.

Antes de comenzar con la lógica veremos cómo nos quedó la parte visual de nuestra app.

image

Lo primero que debemos hacer es captar el evento que ejecutará el cambio de texto del botón y es el clic, es decir, cuando se le da clic al botón, este llama a su evento onClick, que es el que tenemos que capturar nosotros. Para eso vamos al fichero donde está la clase de que representa al Activity donde pertenece el botón, en nuestro caso se llama PrimeraActivity.java.

image

Lo primero que debemos hacer es declarar un objeto Button que es el que va a representar a nuestro botón de la parte visual en el código. Para esto escribimos el siguiente código:

private Button boton;

Señalar que para usar la clase Button de Android es necesario importar el paquete android.widget.Button como muestra la siguiente línea:

import android.widget.Button;

Después de creado el objeto, vamos a asociar el objeto creado con el botón visual. Esto lo hacemos en el método onCreate de la siguiente forma:

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

boton = (Button)this.findViewById(R.id.button1);
}

Vamos a explicar un poco este código. El método onCreate de la case Activity se lanza cuando se lanza esta activity. Con el método findViewByid del activity lo que hacemos es devolver la referencia del objeto visual que tiene como id, el que se pasa por parámetro. En anteriores artículos hablamos de la clase R. En el código cuando ponemos R.id.button1 estamos accediendo al componente visual que tiene un id igual a button1.

Ahora vamos a capturar el evento onClick del botón. Esto lo haremos de la siguiente forma:

boton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
//Acciones a ejecutar
}
});

Después de saber cómo capturar el evento onClick del botón, solo nos queda cambiar el texto del mismo:

boton.setText(R.string.texto);

Es válido aclarar que texto es un String cualquiera que debemos crear para guardar el texto que vamos a mostrar cuando se de clic en el botón. El método onCreate quedaría así:

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

boton = (Button)this.findViewById(R.id.button1);

boton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
boton.setText(R.string.texto);
}
});
}

Con esto hemos hemos terminado nuestra primera aplicación en Android. En próximos artículos iremos enriqueciendo aún más esta aplicación.

{ Leer Más }


jueves, 25 de diciembre de 2014

Layouts en Android

image

Los layouts en Android son elementos no visuales que se usan para controlar la distribución, posición y dimensiones de los componentes que se inserten en su interior. En Android podemos usar dos formas para declarar layouts: la primera mediante código java y la segunda a partir de ficheros XML. Si comparamos estas dos formas, es sin dudas la segunda la más factible por varias razones entre las que se encuentra que existen herramientas visuales donde el programador o el diseñador pueden, de manera fácil diseñar una interfaz de usuario, otra ventaja es que es importante en términos de arquitectura tener separado la lógica de la interfaz de usuario de forma tal que si necesitamos hacer algunos ajustes en el diseño de la aplicación no conlleve a hacer grandes cambios en la aplicación.

En Android, los ficheros de layout codificados en XML se consideran recursos y se guardan dentro del directorio res/layout del proyecto.

Cada fichero XML está formado por un árbol de elementos que describen la forma en que los componentes y contenedores se acomodarán para definir la parte visual. Cada uno de estos elementos tiene atributos que se denominan propiedades y son las que describen cómo es que deben verse los elementos y su comportamiento en un contenedor.

Para cada elemento del XML se tendrá un nombre correspondiente a la clase base de ldirectorio Android. Por ejemplo si existe un elemento Button en el XML, se deberá tener un objeto de la clase Button en el código java.

Cuando una aplicación requiere hacer referencia desde el código java a uno de los elementos del XML es importante que el elemento tenga un identificador, la convención a utilizar para darle este valor de id es @+id/[nombreElemento] Este nombre tiene que ser único.

Después de haber diseñado la interfaz de usuario, es necesario compilar la aplicación para que en la clase R.java se genere un id que Android maneja de manera interna y que permite llamar a los ficheros desde el código java.

Dentro del método onCreate() de la actividadse escribirá la sentencia setLayoutView() con el parámetro R.layout.[nombreDelFicheroXML]. Si se quiere hacer uso del elemento en específico se usa el método findViewById() y se le pasa como parámetro el id del componente. Por ejemplo:

btn = (Button) findViewById(R.id.btn);

Con esta breve explicación acerca de los layouts y su definición en un fichero XML tenemos algunos elementos para el diseño de una aplicación Android, aunque sería bueno abundar un poco más en el tema para aprovechar más las potencialidades de estos recursos, de forma tal que hagamos aplicaciones más eficientes.

{ Leer Más }


lunes, 15 de diciembre de 2014

Conociendo la estructura de un proyecto Android.

En los artículos anteriores sobre la programación en Android, vimos una breve reseña de lo que es Android y su historia y luego abordamos cómo preparar nuestro ambiente de desarrollo para estar listos para adentrarnos en este fascinante mundo.

Pero antes de escribir una línea de código es importante conocer la estructura de un proyecto Android de forma que a la hora de programar podamos saber de forma exacta donde encontrar cada recurso.

Cuando creamos un proyecto de Android en Eclipse se crean una serie de carpetas y ficheros para luego generar la aplicación. Esta estructura será común para cualquier tipo de aplicaciones, independientemente de la versión para la que la estemos desarrollando o su complejidad.

A continuación veremos una imagen con la estructura inicial de Android:

image

Para un mejor entendimiento vamos a explicar los elementos principales de esta estructura de ficheros.

Carpeta src

Como su nombre lo sugiere, es en esta carpeta donde estará todo el código fuente de la aplicación, la programación de la interfaz gráfica, clases auxiliares, entre otras. En un inicio se creará el código elemental del Activity principal de la aplicación.

Carpeta res

Es en esta carpeta donde estarán los recursos necesarios que se utilizarán en la aplicación como son los textos, las imágenes, los videos, entre otros. Dentro de la carpeta res existen otras carpetas con el objetivo de organizar aún mejor los recursos.

image

Vamos a dar un breve repaso de lo que son cada una de destas carpetas

drawable: Contiene las imágenes. Se puede dividir en drawable-ldpi, drawable-mdpi y drawable-hdpi. Los recursos se pondrán en cada una de estas carpetas dependiendo de la resolución del dispositivo que esté consumiendo la aplicación.

layout: Contiene la estructura XML de las pantallas de la interfaz gráfica. Esta carpeta se puede subdividir en layout y layaout-land para diferenciar por la orientación del dispositivo.

anim: Contiene las animaciones de la aplicación.

menú: Contiene la definición de los menús de la aplicación.

values: Contiene recursos como son los textos, los estilos, los colores, entre otros.

xml: Contiene los ficheros XML utilizados por la aplicación.

raw: Contiene recursos adicionales que no sean XML.

Carpeta gen

Es en esta carpeta donde se almacena el código que se genera automáticamente al compilar el proyecto. De estos códigos es importante particularizar en el fichero R.java que es la que contiene la clase R que es donde se almacenan una serie de constantes con los ID de los recursos incluidos en la carpeta res. Esto nos posibilita que desde el código podamos acceder fácilmente a estos recursos.

Carpeta assets

En esta carpeta se van a almacenar los ficheros auxiliares. La diferencia de estos ficheros con los de que se pondrán en la carpeta res/draw es que estos últimos se podrán acceder desde el código a través de la ya mencionada clase R mientras que los de la carpeta assets se tendrá que acceder a través de su ruta.

Fichero AndroidManifest.xml

Es un fichero escrito en XML con los aspectos principales de la aplicación. Ahí nos vamos a encontrar el nombre, los componentes que usan, los permisos de la aplicación entre otras cosas.

Esto es lo esencial de la estructura de ficheros de un proyecto Android, conociendo esto, ya estaremos listos para próximamente desarrollar una pequeña aplicación para Android.

{ Leer Más }


martes, 9 de diciembre de 2014

Entorno de desarrollo en android.

clip_image001

En este tutorial aprenderemos cómo preparar el entorno de trabajo para comenzar a hacer aplicaciones para el sistema operativo Android.

Lo primero que debemos hacer es descargar los archivos y programas necesarios:

· IDE Eclipse que se puede descargar desde este link.

· SDK de Android que se puede descargar en la página de Android developers. Este paquete incluye muchas herramientas que nos van a hacer de mucha utilidad a la hora de programar para Android.

· Plugin ADT, que es un plugin para hacer de eclipse un ide ideal para hacer aplicaciones Android.

Primero debemos comprobar que nuestra computadora tienes los requerimientos mínimos que son necesarios para que todo se instale bien. Esto se puede hacer leyéndolo en la página de Android developers.

Si el eclipse que tenemos está en un compactado, lo extraemos y lo ponemos en cualquier carpeta de nuestra PC. Cuando ejecutamos el eclipse lo primero que nos pide es una dirección que es donde el eclipse guardará todos los proyectos que vayamos creando. Si queremos que esto no nos lo pida más pues elegimos la opción de no preguntar de nuevo.

Ahora vamos a encargarnos del SDK. Este puede estar en un compactado o en un ejecutable. Si fuera un compactado pues lo extraemos en lo ponemos en cualquier carpeta de nuestra PC, si es un ejecutable pues en uno de los pasos nos pedirá la dirección de la carpeta hacia donde queremos instalar el SDK.

Es importante señalar que para que todo esto que estamos montando funcione es necesario el entorno Java por lo que busca los instaladores necesarios para que esto esté correctamente instalado en tu PC.

Para instalar el ADT podemos hacerlo de dos formas, la primera es en nuestro Eclipse vamos a la opción Help -> Install New Software… Damos un click sobre el botón Add, en el cuadro de Add Repository escribimos “ADT Plugin” en el campo Name y en Location escribimos la URL https://dl-ssl.google.com/android/eclipse/. Otra forma de hacer esto es descargarndo el plugin de internet y obteniéndolo de alguna otra vía y copiando en la carpeta plugins que está en el directorio donde esta intalado nuestro Eclipse.

Ahora vamos a decirle al Eclipse dónde es que está el SDK de Android. Para esto vamos a la opción Window -> Preferences… ahí seleccionamos la opción Android del panel de la izquierda y seleccionamos Browse… donde elegiremos la carpeta donde pusimos o instalamos nuestro SDK.

Ya casi tenemos listo nuestro entorno de desarrollo, ahora vamos a descargar los componentes esenciales de SDK. El SDK separa los elementos principales en componentes que se pueden instalar por separado. Para poder desarrollar en Android necesitamos al menos descargar una plataforma Android con sus herramientas asociadas por lo que vamos a Windows -> Android SDK and AVD Manager y elegimos Avaible packages en el panel de la izquierda. En el panel de la derecha vamos a poder ver todas las opciones disponibles para descargar.

clip_image002

De este listado podrás seleccionar todas aquellas que quieras. Las plataformas se adaptan a la versión de Android por lo que si su teléfono tiene instalado Android 2.2 pues deberá descargar esta plataforma o una de las inferiores para poder probar las aplicaciones en él. Dependiendo del número de plataformas y componentes que hayas decidido descargarte, esta parte puede tardarte un poco.

Con estos pasos hemos configurado nuestro entorno para comenzar a desarrollar para Android. Ya estamos listo para hacer nuestra primera aplicación. Espero que les haya sido de ayuda.

{ Leer Más }


lunes, 14 de abril de 2014

Ejemplo Mandelbrot, programación concurrente con el Framework Qt.

clip_image002

El ejemplo Mandelbrot, demuestra la programación concurrente usando el Framework Qt, específicamente explica cómo usar un hilo trabajador para realizar operaciones computacionales complejas sin bloquear el hilo principal del ciclo de eventos [1].

clip_image004

El cómputo pesado en el presente ejemplo es el conjunto Mandelbrot, probablemente el fractal más famoso del mundo.

En la vida real, el enfoque aquí descrito es aplicable a una amplia gama de problemas, incluyendo la red de E / S síncrona y acceso a base de datos, donde la interfaz de usuario debe ser capaz de responder mientras una operación pesada está teniendo lugar.

La aplicación Mandelbrot admite zoom y el scroll con el ratón o el teclado. Para evitar la congelación de bucle de eventos del hilo principal (y, en consecuencia, la interfaz de usuario de la aplicación), ponemos todo el cálculo fractal en un subproceso de trabajo independiente. El hilo emite una señal cuando se termina de hacer la representación del fractal.

Durante el tiempo en que el subproceso de trabajo esta recalculando el fractal para reflejar la nueva posición factor de zoom, el hilo principal, simplemente escala el pixmap previamente representado para proporcionar una respuesta inmediata. El resultado no se ve tan bueno como lo que el subproceso de trabajo con el tiempo termina dando, pero al menos hace que la aplicación sea más sensible. En la secuencia de imágenes que a continuación mostramos se ve: la imagen original, la imagen escalada, y la imagen representada nuevamente.

clip_image006clip_image008 clip_image009

Del mismo modo, cuando el usuario se desplaza, el mapa de pixeles anterior se desplaza inmediatamente, revelando las áreas no pintadas más allá del borde del mapa de píxeles, mientras que la imagen se representa por el subproceso de trabajo.

clip_image011 clip_image013 clip_image015

La aplicación consta de dos clases:

· RenderThread es una subclase QThread que representa (render) el conjunto de Mandelbrot.

· MandelbrotWidget es una subclase QWidget que muestra el conjunto de Mandelbrot en la pantalla y permite al usuario las operaciones de ampliar (zoom) y desplazar (scroll).

Definición de la clase RenderThread

Vamos a empezar con la definición de la clase RenderThread:

class RenderThread : public QThread
{
Q_OBJECT

public:
RenderThread(QObject *parent = 0);
~RenderThread();

void render(double centerX, double centerY, double scaleFactor, QSize resultSize);

signals:
void renderedImage(const QImage &image, double scaleFactor);

protected:
void run();

private:
uint rgbFromWaveLength(double wave);

QMutex mutex;
QWaitCondition condition;
double centerX;
double centerY;
double scaleFactor;
QSize resultSize;
bool restart;
bool abort;

enum { ColormapSize = 512 };
uint colormap[ColormapSize];
};

La clase hereda de QThread de esa forma adquiere la capacidad de ejecutarse en un subproceso independiente. Además del constructor y destructor, render () es la única función pública. Cada vez que el hilo realiza la representación de una imagen, emite la señal renderedImage.

La función protegida run () se reimplementada de QThread. Se llama automáticamente cuando se inicia el hilo.

En la sección privada, tenemos un QMutex, un QWaitCondition, y algunos otros miembros de datos. La exclusión mutua protege al otro miembro de datos.

Implementación de la clase RenderThread

RenderThread::RenderThread(QObject *parent)
: QThread(parent)
{
restart = false;
abort = false;

for (int i = 0; i < ColormapSize; ++i)
colormap[i] = rgbFromWaveLength(380.0 + (i * 400.0 / ColormapSize));
}

En el constructor, inicializamos las operaciones de reiniciar y abortar las variables en false. Estas variables controlan el flujo de la función run (). También se inicia arreglo de mapa de colores, que contiene una serie de colores RGB


 

RenderThread::~RenderThread()
{
mutex.lock();
abort = true;
condition.wakeOne();
mutex.unlock();

wait();
}

El destructor se puede llamar en cualquier momento mientras el hilo está activo. Fijamos abortar en true para decirle a run () que debe detener la ejecución tan pronto como sea posible. También hacemos una llamada a QWaitCondition::wakeOne() para despertar el hilo si está durmiendo. (Como veremos cuando examinemos run (), el hilo es puesto a dormir cuando no tiene nada que hacer.)

Lo importante a notar aquí es que run () se ejecuta en su propio hilo (el subproceso de trabajo), mientras que el constructor y el destructor RenderThread (así como la función render ()) son llamados por el subproceso que creó el subproceso de trabajo. Por lo tanto, necesitamos un mutex para proteger los accesos a las variables de abortar y condición, que pueden ser leídas en cualquier momento el método run ().

Al final del destructor, que llamamos QThread::wait() para esperar a que el método run () ha salido antes de invocar el destructor de la clase base.

void RenderThread::render(double centerX, double centerY, double scaleFactor,
QSize resultSize)
{
QMutexLocker locker(&mutex);

this->centerX = centerX;
this->centerY = centerY;
this->scaleFactor = scaleFactor;
this->resultSize = resultSize;

if (!isRunning()) {
start(LowPriority);
} else {
restart = true;
condition.wakeOne();
}
}

La función render () es llamada por el MandelbrotWidget cada vez que necesita generar una nueva imagen del conjunto de Mandelbrot. Los parámetros CenterX, CenterY y scaleFactor especifican la parte del fractal para representar; resultSize especifica el tamaño de la QImage resultante.

La función almacena los parámetros de las variables miembro. Si el hilo no se está ejecutando, se inicia; de otro modo, se establece el reinicio a true (diciendo al run () que debe detener cualquier cálculo sin terminar y empezar de nuevo con los nuevos parámetros) y se despierta el hilo, que podría estar durmiendo.

void RenderThread::run()
{
forever {
mutex.lock();
QSize resultSize = this->resultSize;
double scaleFactor = this->scaleFactor;
double centerX = this->centerX;
double centerY = this->centerY;
mutex.unlock();

La función run() es muy grande, por lo cual se divide en dos partes. El cuerpo de la función es un bucle infinito que se inicia mediante el almacenamiento de los parámetros de representación en variables locales. Como de costumbre, protegemos los accesos a las variables miembro utilizando el mutex de la clase. El almacenamiento de las variables miembro en variables locales nos permite minimizar la cantidad que de código que necesita ser protegido por un mutex. Esto asegura que el hilo principal nunca tendrá que bloquear durante demasiado tiempo cuando se necesita acceder a las variables miembros de RenderThread (por ejemplo, en el render ()). La palabra clave forever, es como foreach, una seudo-palabra clave de Qt.

int halfWidth = resultSize.width() / 2;
int halfHeight = resultSize.height() / 2;
QImage image(resultSize, QImage::Format_RGB32);

const int NumPasses = 8;
int pass = 0;
while (pass < NumPasses) {
const int MaxIterations = (1 << (2 * pass + 6)) + 32;
const int Limit = 4;
bool allBlack = true;

for (int y = -halfHeight; y < halfHeight; ++y) {
if (restart)
break;
if (abort)
return;

uint *scanLine =
reinterpret_cast<uint *>(image.scanLine(y + halfHeight));
double ay = centerY + (y * scaleFactor);

for (int x = -halfWidth; x < halfWidth; ++x) {
double ax = centerX + (x * scaleFactor);
double a1 = ax;
double b1 = ay;
int numIterations = 0;

do {
++numIterations;
double a2 = (a1 * a1) - (b1 * b1) + ax;
double b2 = (2 * a1 * b1) + ay;
if ((a2 * a2) + (b2 * b2) > Limit)
break;

++numIterations;
a1 = (a2 * a2) - (b2 * b2) + ax;
b1 = (2 * a2 * b2) + ay;
if ((a1 * a1) + (b1 * b1) > Limit)
break;
} while (numIterations < MaxIterations);

if (numIterations < MaxIterations) {
*scanLine++ = colormap[numIterations % ColormapSize];
allBlack = false;
} else {
*scanLine++ = qRgb(0, 0, 0);
}
}
}

if (allBlack && pass == 0) {
pass = 4;
} else {
if (!restart)
emit renderedImage(image, scaleFactor);
++pass;
}
}

Luego viene el núcleo del algoritmo. En lugar de tratar de crear una imagen perfecta del conjunto de Mandelbrot, hacemos varias pasadas y generamos más y más precisas (y computacionalmente costosas) aproximaciones del fractal.

Si descubrimos dentro del bucle que la variable restart se ha establecido en true (por render ()), rompemos el bucle de inmediato, por lo que el control vuelve rápidamente a la parte superior del bucle externo (el bucle forever) y buscamos a los nuevos parámetros de renderizado. Del mismo modo, si descubrimos que la variable abort se ha establecido en true (por el destructor RenderThread), salimos de la función de inmediato, terminando el hilo.

El algoritmo principal está más allá del alcance de este tutorial.

        mutex.lock();
if (!restart)
condition.wait(&mutex);
restart = false;
mutex.unlock();
}
}

Una vez que hemos terminado con todas las iteraciones, llamamos QWaitCondition::wait() para poner el hilo a dormir mediante el llamado, a menos que la variable restart tenga valor true. No tiene sentido mantener un subproceso de trabajo en bucle indefinidamente mientras no hay nada que hacer.

uint RenderThread::rgbFromWaveLength(double wave)
{
double r = 0.0;
double g = 0.0;
double b = 0.0;

if (wave >= 380.0 && wave <= 440.0) {
r = -1.0 * (wave - 440.0) / (440.0 - 380.0);
b = 1.0;
} else if (wave >= 440.0 && wave <= 490.0) {
g = (wave - 440.0) / (490.0 - 440.0);
b = 1.0;
} else if (wave >= 490.0 && wave <= 510.0) {
g = 1.0;
b = -1.0 * (wave - 510.0) / (510.0 - 490.0);
} else if (wave >= 510.0 && wave <= 580.0) {
r = (wave - 510.0) / (580.0 - 510.0);
g = 1.0;
} else if (wave >= 580.0 && wave <= 645.0) {
r = 1.0;
g = -1.0 * (wave - 645.0) / (645.0 - 580.0);
} else if (wave >= 645.0 && wave <= 780.0) {
r = 1.0;
}

double s = 1.0;
if (wave > 700.0)
s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0);
else if (wave < 420.0)
s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0);

r = pow(r * s, 0.8);
g = pow(g * s, 0.8);
b = pow(b * s, 0.8);
return qRgb(int(r * 255), int(g * 255), int(b * 255));
}

La función rgbFromWaveLength () es una función auxiliar que convierte una longitud de onda a un valor RGB compatible con QImages 32 bits. Se llama desde el constructor para inicializar el arreglo de mapa de colores con colores agradables.

 

Definición de la clase MandelbrotWidget


La clase MandelbrotWidget utiliza RenderThread para dibujar el conjunto de Mandelbrot en la pantalla. Aquí está la definición de la clase:

class MandelbrotWidget : public QWidget
{
Q_OBJECT

public:
MandelbrotWidget(QWidget *parent = 0);

protected:
void paintEvent(QPaintEvent *event);
void resizeEvent(QResizeEvent *event);
void keyPressEvent(QKeyEvent *event);
#ifndef QT_NO_WHEELEVENT
void wheelEvent(QWheelEvent *event);
#endif
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);

private slots:
void updatePixmap(const QImage &image, double scaleFactor);
void zoom(double zoomFactor);

private:
void scroll(int deltaX, int deltaY);

RenderThread thread;
QPixmap pixmap;
QPoint pixmapOffset;
QPoint lastDragPos;
double centerX;
double centerY;
double pixmapScale;
double curScale;
};

El widget reimplementa muchos controladores de eventos de QWidget. Además, cuenta con un método slot updatePixmap () que realiza la conexión a la señal renderedImage () del subproceso de trabajo para actualizar la pantalla cada vez que lleguen nuevos datos desde el hilo.

Entre las variables privadas, tenemos hilo de tipo RenderThread y mapa de píxeles, que contiene la última imagen renderizada.

Implementación de la clase MandelbrotWidget

const double DefaultCenterX = -0.637011f;
const double DefaultCenterY = -0.0395159f;
const double DefaultScale = 0.00403897f;

const double ZoomInFactor = 0.8f;
const double ZoomOutFactor = 1 / ZoomInFactor;
const int ScrollStep = 20;

La aplicación se inicia con algunas constantes que vamos a necesitar más adelante.

MandelbrotWidget::MandelbrotWidget(QWidget *parent)
: QWidget(parent)
{
centerX = DefaultCenterX;
centerY = DefaultCenterY;
pixmapScale = DefaultScale;
curScale = DefaultScale;

connect(&thread, SIGNAL(renderedImage(QImage,double)), this, SLOT(updatePixmap(QImage,double)));

setWindowTitle(tr("Mandelbrot"));
#ifndef QT_NO_CURSOR
setCursor(Qt::CrossCursor);
#endif
resize(550, 400);

}

La parte interesante del constructor es la llamada a los métodos qRegisterMetaType() y QObject::connect(). Vamos a empezar con la llamada al connect().

Aunque se parece a una conexión de señal-ranura estándar entre dos QObjects, ya que la señal se emite en un subproceso distinto al subproceso donde vive el receptor, la conexión es efectivamente una conexión en cola (queued connection). Estas conexiones son asíncronas (es decir, no-bloqueantes), significa que el slot será llamado en algún momento después de la declaración emit. Lo que es más, la ranura se invocará en el hilo en el que el receptor vive. Aquí, la señal se emite en el subproceso de trabajo, y la ranura se ejecuta en el hilo GUI cuando el control vuelve al bucle de eventos.

Con conexiones en cola, Qt debe guardar una copia de los argumentos que se han pasado a la señal para que pueda pasarlos a la ranura más adelante. Qt sabe tomar de copia de muchos tipos de C + + y Qt, pero QImage no es uno de ellos. Por tanto, debemos llamar a la función de plantilla qRegisterMetaType() antes de que podamos utilizar QImage como parámetro de conexiones de cola.

void MandelbrotWidget::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
painter.fillRect(rect(), Qt::black);

if (pixmap.isNull()) {
painter.setPen(Qt::white);
painter.drawText(rect(), Qt::AlignCenter, tr("Rendering initial image, please wait..."));
return;
}

En paintEvent(), se comienza por llenar el fondo con negro. Si no tenemos nada aún para pintar (mapa de pixels es nulo), se muestra un mensaje en el widget que pide al usuario que ser paciente y termina la función de inmediato.

    if (curScale == pixmapScale) {
painter.drawPixmap(pixmapOffset, pixmap);
} else {
double scaleFactor = pixmapScale / curScale;
int newWidth = int(pixmap.width() * scaleFactor);
int newHeight = int(pixmap.height() * scaleFactor);
int newX = pixmapOffset.x() + (pixmap.width() - newWidth) / 2;
int newY = pixmapOffset.y() + (pixmap.height() - newHeight) / 2;

painter.save();
painter.translate(newX, newY);
painter.scale(scaleFactor, scaleFactor);
QRectF exposed = painter.matrix().inverted().mapRect(rect()).adjusted(-1, -1, 1, 1);
painter.drawPixmap(exposed, pixmap, exposed);
painter.restore();
}

Si el mapa de píxeles tiene el factor de escala correcto, podemos extraer el mapa de píxeles directamente sobre el widget. De lo contrario, podemos aumentar la escala y traducimos el sistema de coordenadas (coordinate system) antes de sacar el mapa de pixels. Mediante el mapeo inverso rectángulo del widget usando la matriz de pintado a escala, también nos aseguramos de que sólo las áreas expuestas del mapa de píxeles se dibujan. Las llamadas a QPainter::save() y QPainter::restore() aseguran de que todo el proceso de pintado que se realiza después utiliza el sistema de coordenadas estándar.

QString text = tr("Use mouse wheel or the '+' and '-' keys to zoom. "
"Press and hold left mouse button to scroll.");
QFontMetrics metrics = painter.fontMetrics();
int textWidth = metrics.width(text);

painter.setPen(Qt::NoPen);
painter.setBrush(QColor(0, 0, 0, 127));
painter.drawRect((width() - textWidth) / 2 - 5, 0, textWidth + 10, metrics.lineSpacing() + 5);
painter.setPen(Qt::white);
painter.drawText((width() - textWidth) / 2, metrics.leading() + metrics.ascent(), text);
}

Al final del controlador de eventos de pintura, trazamos una cadena de texto y un rectángulo semitransparente en la parte superior del fractal.

void MandelbrotWidget::resizeEvent(QResizeEvent * /* event */)
{
thread.render(centerX, centerY, curScale, size());
}

Cada vez que el usuario cambia el tamaño del widget, llamamos al método render () para comenzar a generar una nueva imagen, con los mismo parámetros: centerX, CenterY y curScale pero con el nuevo tamaño de widget.

Nótese que confiamos en que resizeEvent () se llama automáticamente por Qt cuando el widget se muestra la primera vez para generar la imagen del primer momento.

void MandelbrotWidget::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Plus:
zoom(ZoomInFactor);
break;
case Qt::Key_Minus:
zoom(ZoomOutFactor);
break;
case Qt::Key_Left:
scroll(-ScrollStep, 0);
break;
case Qt::Key_Right:
scroll(+ScrollStep, 0);
break;
case Qt::Key_Down:
scroll(0, -ScrollStep);
break;
case Qt::Key_Up:
scroll(0, +ScrollStep);
break;
default:
QWidget::keyPressEvent(event);
}
}

El manejador del evento de presionar una tecla proporciona algunas asociaciones de teclas para el beneficio de los usuarios que no disponen de un ratón. Las funciones zoom () y scroll () serán cubiertos más adelante.

void MandelbrotWidget::wheelEvent(QWheelEvent *event)
{
int numDegrees = event->delta() / 8;
double numSteps = numDegrees / 15.0f;
zoom(pow(ZoomInFactor, numSteps));
}

El controlador de eventos de la rueda es reimplementada para hacer el control nivel de zoom con la rueda del ratón. QWheelEvent::delta() devuelve el ángulo del movimiento de la rueda del ratón, en octavos de un grado. Para la mayoría de los ratones, un paso de la rueda corresponde a 15 grados. Averiguamos cuántos pasos ratón tenemos y determinamos el factor de zoom en consecuencia. Por ejemplo, si tenemos dos pasos de rueda en la dirección positiva (es decir, 30 grados), el factor de zoom se vuelve ZoomInFactor a la segunda potencia, es decir, 0,8 * 0,8 = 0,64.

void MandelbrotWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
lastDragPos = event->pos();
}

Cuando el usuario pulsa el botón izquierdo del ratón, almacenamos la posición del puntero del ratón en lastDragPos.

void MandelbrotWidget::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
pixmapOffset += event->pos() - lastDragPos;
lastDragPos = event->pos();
update();
}
}

Cuando el usuario mueve el puntero del ratón mientras se mantiene pulsado el botón izquierdo del ratón, ajustamos pixmapOffset para pintar el mapa de pixels en una posición desplazada y se llama al QWidget::update() para forzar un repintado.

void MandelbrotWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
pixmapOffset += event->pos() - lastDragPos;
lastDragPos = QPoint();

int deltaX = (width() - pixmap.width()) / 2 - pixmapOffset.x();
int deltaY = (height() - pixmap.height()) / 2 - pixmapOffset.y();
scroll(deltaX, deltaY);
}
}

Al soltar el botón izquierdo del ratón, actualizamos pixmapOffset tal como lo hicimos en un movimiento del ratón y reiniciamos lastDragPos a un valor predeterminado. A continuación, hacemos un llamado de scroll () para representar una nueva imagen para la nueva posición. (El ajuste de pixmapOffset no es suficiente, ya que las áreas reveladas al arrastrar el mapa de píxeles se dibujan en negro.)

void MandelbrotWidget::updatePixmap(const QImage &image, double scaleFactor)
{
if (!lastDragPos.isNull())
return;

pixmap = QPixmap::fromImage(image);
pixmapOffset = QPoint();
lastDragPos = QPoint();
pixmapScale = scaleFactor;
update();
}

El slot updatePixmap () se invoca cuando el subproceso de trabajo ha terminado de representar una imagen. Empezamos comprobando si un lastre está en vigor y no hacemos nada en ese caso. En el caso normal, guardamos la imagen en mapa de pixels e inicializar algunos de los otros miembros. Al final, llamamos QWidget::update() para actualizar la pantalla.

En este punto, uno podría preguntarse por qué usamos un QImage para el parámetro y un QPixmap para el miembro de datos. ¿Por qué no se adhieren a un tipo? La razón es que QImage es la única clase que admite la manipulación de píxeles directa, lo que necesitamos en el subproceso de trabajo. Por otro lado, antes de que una imagen se pueda dibujar en la pantalla, debe ser convertida en un mapa de pixels. Es mejor hacer la conversión de una vez por todas aquí, y no en paintEvent ().

void MandelbrotWidget::zoom(double zoomFactor)
{
curScale *= zoomFactor;
update();
thread.render(centerX, centerY, curScale, size());
}

En el zoom (), se recalcula curScale. Entonces llamamos QWidget::update() para dibujar un mapa de pixels a escala, y le pedimos al subproceso de trabajo para hacer una nueva imagen correspondiente al nuevo valor curScale.

void MandelbrotWidget::scroll(int deltaX, int deltaY)
{
centerX += deltaX * curScale;
centerY += deltaY * curScale;
update();
thread.render(centerX, centerY, curScale, size());
}

scroll() es similar a zoom(), excepto que los parámetros afectados son centerX y CenterY.

 

La función main()


La naturaleza multiproceso de la aplicación no tiene impacto en su función main (), que es tan simple como siempre:

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MandelbrotWidget widget;
widget.show();
return app.exec();
}

El código fuente de esta aplicación pueden descargarlo desde aquí.

Esto es todo por hoy, esperamos que este artículo le haya sido útil para aprender a usar un hilo trabajador para realizar operaciones computacionales complejas sin bloquear el hilo principal del ciclo de eventos. En próximas entradas de nuestro blog estaremos profundizando sobre otros temas de programación concurrente usando el framework Qt, que permite grandes potencialidades para el desarrollo de aplicaciones con excelente rendimiento.




{ Leer Más }


viernes, 28 de marzo de 2014

Principios básicos de la programación concurrente usando el Framework Qt.

clip_image002

El presente artículo tiene el objetivo de mostrar los principios básicos de la programación multi-hilo usando el Framework Qt.

Los hilos permiten hacer que los sistemas ejecuten tareas en paralelo, al igual que procesos. Entonces, ¿cómo se diferencian los hilos de los procesos? Mientras que usted está haciendo los cálculos en una hoja de cálculo, puede haber también un reproductor multimedia que se ejecuta en el mismo escritorio tocando su canción favorita. Aquí hay un ejemplo de dos procesos trabajando en paralelo: uno que ejecuta el programa de hoja de cálculo, una ejecución de un reproductor de música. La multitarea es un término muy conocido para esto. Una mirada más cercana al reproductor de música revela que allí son cosas que suceden en paralelo dentro de un único proceso. Mientras reproductor de música realiza el envío de la música para el controlador de audio, la interfaz de usuario con todas sus campanas y silbatos se actualiza constantemente. Para esto son los hilos - la concurrencia dentro de un único proceso [1].

Entonces, ¿cómo se implementa la concurrencia? Trabajo paralelo en las CPU de un solo núcleo, es una ilusión que es algo similar a la ilusión de imágenes en movimiento en el cine. Para los procesos, la ilusión se produce mediante la interrupción de trabajo del procesador en un proceso después de un tiempo muy corto. A continuación, el procesador se mueve al siguiente proceso. Para cambiar entre procesos, el contador de programa actual se guarda y contador de programa del próximo procesador se carga. Esto no es suficiente, ya que el mismo hay que hacer con los registros y cierta arquitectura y datos específicos del sistema operativo.

Así como una CPU puede alimentar dos o más procesos, también es posible dejar que la CPU ejecute en dos segmentos de código diferentes de un único proceso. Cuando se inicia un proceso, siempre se ejecuta un segmento de código y por lo tanto, se dice que el proceso ejecuta un hilo. Sin embargo, el programa puede optar por iniciar un segundo hilo. Entonces, dos secuencias de códigos diferentes se procesan simultáneamente dentro de un solo proceso. La concurrencia se logra en las CPU de un solo núcleo guardando repetidamente contadores de programa y luego los registros cargan contadores y registros del programa del próximo hilo. No se requiere la cooperación del programa para desplazarse entre los hilos activos. Un hilo puede estar en cualquier estado cuando se produce el cambio al siguiente contexto.

La tendencia actual en el diseño de la CPU es tener varios núcleos. Una aplicación de un único subproceso típico puede hacer uso de un solo núcleo. Sin embargo, un programa con múltiples hilos se puede asignar a varios núcleos, lo que hace que las cosas sucedan de una manera verdaderamente concurrente. Como resultado, la distribución del trabajo a más de un hilo puede hacer que un programa funcione mucho más rápido en las CPU multinúcleo debido a que núcleos adicionales pueden ser utilizados.

Hilo GUI e Hilo Trabajador

Como se mencionó, cada programa tiene un hilo cuando se inicia. Este hilo se llama el "hilo conductor" (también conocido como el "hilo GUI" en aplicaciones Qt). La interfaz gráfica de usuario Qt se debe ejecutar en este hilo. Todos los widgets y varias clases relacionadas, por ejemplo QPixmap, no trabajan en los hilos secundarios. Un subproceso secundario que comúnmente se conoce como un "subproceso de trabajo" ya que se utiliza para descargar el trabajo de procesamiento del hilo principal.

El acceso simultáneo a los datos

Cada hilo tiene su propia pila, lo que significa que cada hilo tiene su propio historial de llamadas y variables locales. A diferencia de los procesos, los hilos comparten el mismo espacio de direcciones. El siguiente diagrama muestra cómo se encuentran los bloques de construcción de los hilos en la memoria. Contador y los registros de hilos inactivos Programa suelen mantenerse en el espacio del núcleo. Hay una copia compartida del código y una pila separada para cada hilo.

clip_image004

Si dos hilos tienen un puntero al mismo objeto, es posible que los dos hilos tengan acceso a dicho objeto al mismo tiempo y esto potencialmente puede destruir la integridad del objeto. Es fácil imaginar las muchas cosas que pueden salir mal cuando dos métodos del mismo objeto se ejecutan simultáneamente.

A veces es necesario para acceder a un objeto a partir de diferentes hilos, por ejemplo, cuando los objetos que viven en diferentes hilos necesitan comunicarse. Desde hilos utilizan el mismo espacio de direcciones, es más fácil y más rápido para hilos para intercambiar datos de lo que es para los procesos. Los datos no tiene que ser serializado y copiado. Pasar punteros es posible, pero debe haber una coordinación estricta de lo que toques de hilo que se oponen. La ejecución simultánea de las operaciones en un objeto debe ser prevenida. Hay varias maneras de lograr esto y algunas de ellos se describen a continuación.

Entonces, ¿Cómo se pueden programar estas aplicaciones de manera segura? Todos los objetos creados en un hilo se pueden utilizar de manera segura dentro de ese hilo, siempre que otros hilos no tengan referencias a los mismos y los objetos no tienen acoplamiento implícito con otros hilos. Tal acoplamiento implícito puede suceder cuando los datos son compartidos entre instancias como miembros estáticos, singletons o datos globales. Debe familiarizarse con el concepto de clase hilo y funciones seguras y de reentrada.

Uso de hilos

Básicamente, existen dos escenarios fundamentales para el uso de los hilos:

  • Hacer un procesamiento más rápido, haciendo uso de los procesadores multinúcleo.
  • Mantener el hilo GUI u otros hilos críticos liberados al descargar el procesamiento de larga duración o el bloqueo de llamadas a otros hilos.


¿Qué Tecnología Qt se debe usar para el manejo de hilos?

A veces quieres hacer algo más que la ejecución de un método en el contexto de otro hilo. Es posible que desee tener un objeto que vive en otro hilo que proporciona un servicio al hilo GUI. Tal vez usted quiere otro hilo que siga con vida para siempre para sondear los puertos de hardware y enviar una señal al hilo GUI cuando algo notable ha sucedido. Qt proporciona diferentes soluciones para el desarrollo de aplicaciones con subprocesos. La solución correcta depende de la finalidad del nuevo hilo, así como el tiempo de vida que se requiere de ese hilo:

Vida útil del hilo

Tarea de Desarrollo

Solución

Una llamada

Ejecutar un método dentro de otro hilo y dejar el hilo cuando termine el método.

Qt proporciona diferentes soluciones:

Escribir una función y ejecutarla con QtConcurrent::run()

Derivar una clase de QRunnable y ejecutarla en el hilo global con QThreadPool::globalInstance()->start()

Derivar una clase de QThread, reimplementar el método QThread::run() y utilizar QThread::start() para ejecutarlo.

Una llamada

Operaciones se han de realizar en todos los hilos de un contenedor. El procesamiento deberá ser realizado utilizando todos los núcleos disponibles. Un ejemplo común es la producción de imágenes en miniatura de una lista de imágenes.

QtConcurrent proporciona la función de mapa () para aplicar las operaciones en cada elemento contenedor, filtro () para la selección de elementos de recipiente, y la opción de especificar una función de reducir para combinar los elementos restantes.

Una llamada

Una operación de larga duración tiene que ser puesto en otro hilo. Durante el curso de procesamiento, información de estado se debe enviar al hilo de la interfaz gráfica de usuario.

Utilice QThread, reimplementar run() y también las signal/slot sean necesarias. Conecte las señales a las ranuras (slots) del hilo GUI utilizando conexiones de señal / ranura (signal/slot) en cola.

Permanente

Tener un objeto de estar en otro hilo y dejar que se realicen diferentes tareas bajo petición. Esto significa que la comunicación hacia y desde el subproceso de trabajo que se requiere.

Derivar una clase de QObject e implementar las ranuras y las señales necesarias, mover el objeto a un hilo con un bucle de eventos de correr y comunicarse con el objeto a través de conexiones de señal / ranura en cola.

Permanente

Tener un objeto que vive en otro hilo, dejar al objeto realizar tareas repetitivas, como el sondeo de puertos y permitir la comunicación con el hilo GUI.

Igual que el anterior, pero también utilizar un temporizador en el subproceso de trabajo para implementar la encuesta. Sin embargo, la mejor solución para la encuesta es evitarla por completo. A veces, usar QSocketNotifier es una alternativa.

En próximas entradas estaremos profundizando sobre otros temas de programación concurrente usando el framework Qt.

{ Leer Más }


viernes, 20 de septiembre de 2013

¿Ingeniero de Software o Programador?

Hola comunidad. Decidí comenzar, con la pregunta que da título a este, una serie de artículos sobre ingeniería de software, programación y buenas prácticas en sentido general. A pesar del tiempo la interrogante sigue siendo polémica; fundamentalmente cuando nos estamos introduciendo en una especialidad tan amplia como la informática donde, muchas veces, desde cada posición se tiende a menospreciar la importancia del trabajo del otro.

clip_image002Y la pregunta en cuestión para mí sólo tiene una respuesta: AMBOS. No sólo porque son imprescindibles para desarrollar un software de calidad, sino porque, si somos capaces de conocer ambos desempeños seremos mucho mejores programadores e ingenieros de software.

En los inicios de lo que conocemos actualmente como industria del software, los primeros roles establecidos fueron el de analista y programador, en el intento de salir de la era del desarrollo de software “artesanal”. Estos roles aún existen por sus nombres, pero para aquel entonces sus competencias eran mucho más limitadas. El analista tenía como responsabilidad conocer paso a paso la actividad que se deseaba automatizar, la describía en papel detalladamente y era entonces que el programador, con sus conocimientos del lenguaje, las traducía a un programa. Dado que los problemas de comunicación entre estos roles eran considerables, la tendencia fue que el analista conociera de programación para manejar los mismos términos que el programador y que este, a su vez, conociera de la concepción de un programa para que no fuera sólo un traductor, sino un agente activo en la mejora del mismo. Ese proceso de interdisciplinariedad generó una expansión de ambos perfiles y tuvo como resultado el surgimiento de nuevos roles en la ingeniería de un proceso de desarrollo de software cada vez más depurado.

Actualmente, un programador tiene dominio de uno o más lenguajes y sus herramientas, que cada vez son más acabadas y complejas e incluyen definiciones arquitectónicas y patrones de diseño que, inevitablemente, llevan a que el programador esté cada vez más cerca del conocimiento integral del desarrollo de un sistema. Por lo que, si como programador tengo los conocimientos ingenieriles necesarios, se eleva mi rendimiento y obtengo como resultado calidad y eficiencia.

Por otro lado, desde mi punto de vista, un ingeniero de software sin experiencia de programación se verá muy limitado en el desempeño de los diferentes roles para los que supuestamente fue preparado. Pudiera enumerar un conjunto de situaciones frecuentes en las que se vería sumamente limitado su desempeño:

  1. ¿Cómo podría definir el flujo de un proceso a automatizar sin contar con un pensamiento orientado a objetos que dé un acabado al análisis del problema y sea el punto de partida al diseño?
  2. ¿Con qué base pudiera definirse la arquitectura de un sistema y tomar las decisiones arquitectónicas que se requieran en su desarrollo?
  3. ¿Cómo definir un diseño de clases correctamente y decidir que patrones quiero aplicar?
  4. ¿Cómo pudiera crear un diagrama UML de secuencias con las correspondientes llamadas entre clases sin saber que eso que está escribiendo “no compila”?
  5. ¿Cómo pudiera definir correctamente para un problema qué algoritmo utilizar y su complejidad?
  6. ¿Cómo definir correctamente pruebas de caja blanca?
  7. ¿Cómo liderar con efectividad un equipo de programadores sin saber qué es lo que hacen?

En general, salvo algunos roles que pudiera desempeñar con escasos o nulos conocimientos de programación, el ingeniero de software degenera a poco más que a ese primer analista que hablé existía en un principio, negando todos estos años de evolución del proceso de desarrollo de software, donde el ingeniero es quien lo hace posible.

{ Leer Más }


martes, 3 de septiembre de 2013

Programación multi-hilos usando QWaitCondition y QMutex.

clip_image002

En el artículo anterior mostramos los principales elementos a tener en cuenta para realizar programación concurrente usando el Framework Qt, específicamente empleamos la clase QSemaphore para resolver el problema del productor-consumidor. En este nuevo artículo explicaremos otra alternativa que también es muy eficiente para programar aplicaciones multi-hilos, se trata de la combinación de QWaitCondition y QMutex.

El objetivo del artículo es mostrar cómo utilizar QWaitCondition y QMutex para controlar el acceso a un búfer circular que comparten un hilo productor y otro hilo consumidor [1].

El productor escribe datos en el búfer hasta que se llega al final del búfer, en cuyo punto se reinicia desde el principio, sobrescribiendo los datos existentes. El hilo consumidor lee los datos a medida que se produce y lo escribe en la salida de error estándar.

Las condiciones de espera (Wait Condition) hacen que sea posible tener un mayor nivel de concurrencia que el que lograríamos si usáramos solamente mutex. Si los accesos al búfer simplemente fueron vigilados por un QMutex, el hilo consumidor no podría acceder al búfer al mismo tiempo que el hilo productor. Sin embargo, no hay nada malo en tener dos hilos que trabajan en diferentes partes del búfer al mismo tiempo.

El ejemplo consta de dos clases: Productor (Producer) y Consumidor (Consumer). Ambos heredan de QThread. El búfer circular utilizado para la comunicación entre estas dos clases y las herramientas de sincronización que lo protegen son variables globales.

Variables globales

Vamos a empezar por la revisión del búfer circular y las herramientas de sincronización asociadas:

const int DataSize = 100000;

const int BufferSize = 8192;
char buffer[BufferSize];

QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;

DataSize es la cantidad de datos que el productor va a generar. Para mantener el ejemplo tan simple como sea posible, hacemos una constante. BufferSize es el tamaño del búfer circular. Es menos de DataSize, lo que significa que en algún momento el productor alcanzará el final del búfer y reiniciará desde el principio.

Para sincronizar el productor y el consumidor, necesitamos dos condiciones de espera y un mutex. La condición bufferNotEmpty se señala cuando el productor ha generado algunos datos, diciéndole al consumidor que puede empezar a leerlo. La condición bufferNotFull se señala cuando el consumidor haya leído algunos datos, dice la productora que pueda generar mucho más. La variable numUsedBytes indica el número de bytes en el búfer que contiene los datos.

Juntos, las condiciones de espera, la exclusión mutua, y el contador numUsedBytes deben garantizar que el productor nunca está escribiendo bytes en una ubicación mayor que bufferSize por delante del consumidor, y que el consumidor nunca lee los datos que el productor no ha generado todavía.

Clase Productor

Repasemos el código de la clase Producer:

class Producer : public QThread
{
public:
Producer(QObject *parent = NULL) : QThread(parent)
{
}

void run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();

buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];

mutex.lock();
++numUsedBytes;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
};

El productor genera DataSize bytes de datos. Antes de escribir un byte en el búfer circular, debe comprobar primero si el búfer está lleno (es decir, si numUsedBytes es igual a BufferSize). Si el búfer está lleno, el hilo espera en la condición bufferNotFull.

Al final, el productor incrementa la variable numUsedBytes y envía la señal indicando que la condición bufferNotEmpty es verdadera, ya que numUsedBytes es necesariamente mayor que 0.

Guardamos todos los accesos a la variable numUsedBytes con un mutex. Además, la función QWaitCondition::wait() acepta un mutex como argumento. Este mutex está desbloqueado antes el hilo se pone a dormir y bloqueado cuando el hilo se despierta. Por otra parte, la transición desde el estado de bloqueo al estado de espera es atómica, para evitar que se produzcan las condiciones de carrera.

Clase del Consumidor

Volvamos a la clase de los consumidores:

class Consumer : public QThread
{
Q_OBJECT
public:
Consumer(QObject *parent = NULL) : QThread(parent)
{
}

void run()
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == 0)
bufferNotEmpty.wait(&mutex);
mutex.unlock();

fprintf(stderr, "%c", buffer[i % BufferSize]);

mutex.lock();
--numUsedBytes;
bufferNotFull.wakeAll();
mutex.unlock();
}
fprintf(stderr, "\n");
}

signals:
void stringConsumed(const QString &text);
};

El código es muy similar a la del productor. Antes de leer el byte, comprobamos si el búfer está vacío (numUsedBytes es 0) y esperamos en la condición bufferNotEmpty si está vacío. Después de que hemos leído el byte, hacemos el decremento de la variable numUsedBytes (en lugar de incrementarlo), y señalamos la condición bufferNotFull (en lugar de la condición bufferNotEmpty).

La función main ()

En la función main (), creamos los dos hilos y llamamos a la función QThread::wait() para asegurar de que ambos contextos de ejecución tienen tiempo para terminar antes de la salida:

int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}

Entonces, ¿qué sucede cuando ejecutamos el programa? Inicialmente, el hilo productor es el único que puede hacer cualquier cosa, el consumidor está bloqueado en espera de que la condición bufferNotEmpty sea señalizada (numUsedBytes es 0). Una vez que el productor ha puesto un byte en el buffer, numUsedBytes es BufferSize - 1 y la condición bufferNotEmpty se marca. En ese momento, pueden ocurrir dos cosas: o bien el hilo consumidor se hace cargo y lee el byte, o el productor genera un segundo byte.

El modelo productor-consumidor que se presenta en este ejemplo permite escribir aplicaciones multiprocesos altamente concurrentes. En un equipo con varios procesadores, el programa es potencialmente hasta el doble de rápido que el programa equivalente basado en exclusión mutua, ya que los dos hilos pueden estar activos al mismo tiempo en diferentes partes del búfer.

Tenga en cuenta que estos beneficios no siempre son efectivos. Los procesos de bloqueo y desbloqueo de un QMutex tiene un costo. En la práctica, probablemente valdría la pena dividir el búfer en trozos, pues es más eficiente operar en trozos en lugar de bytes individuales. El tamaño del búfer es también un parámetro que se debe seleccionar cuidadosamente, basado en la experimentación.

El código fuente de esta aplicación pueden descargarlo desde aquí.




{ Leer Más }


IconIconIcon