Subprocesamiento múltiple con C y Win32

El compilador de Microsoft C/C++ (MSVC) permite la creación de aplicaciones multiproceso. Considere la posibilidad de usar más de un subproceso si la aplicación necesita realizar operaciones costosas que harían que la interfaz de usuario dejara de responder.

Con MSVC hay varias maneras de programar con varios subprocesos: puede usar C++/WinRT y la biblioteca de Windows Runtime, la biblioteca de Microsoft Foundation Class (MFC), C++/CLI y el runtime de .NET, o la biblioteca en tiempo de ejecución de C y la API Win32. Este artículo trata sobre el subprocesamiento múltiple en C. Para obtener código de ejemplo, vea Ejemplo de programa multiproceso en C.

Programas multiproceso

Un subproceso es básicamente una ruta de acceso de ejecución por medio de un programa. También es la unidad de ejecución más pequeña que programa Win32. Un subproceso consta de una pila, el estado de los registros de CPU y una entrada en la lista de ejecución del programador del sistema. Cada subproceso comparte todos los recursos del proceso.

Un proceso consta de uno o varios subprocesos y del código, los datos y otros recursos de un programa en memoria. Los recursos típicos de un programa son archivos abiertos, semáforos y memoria asignada dinámicamente. Un programa se ejecuta cuando el programador del sistema otorga a uno de sus subprocesos el control de la ejecución. El programador determina qué subprocesos deben ejecutarse y cuándo. Es posible que los subprocesos de menor prioridad tengan que esperar mientras los de mayor prioridad completan sus tareas. En las máquinas multiprocesador, el programador puede mover subprocesos individuales a distintos procesadores para equilibrar la carga de la CPU.

Cada subproceso de un proceso funciona de manera independiente. A menos que los haga visibles entre sí, los subprocesos se ejecutan individualmente y no son conscientes de los demás subprocesos de un proceso. Pero los subprocesos que comparten recursos comunes deben coordinar su trabajo mediante semáforos u otro método de comunicación entre procesos. Para obtener más información sobre la sincronización de subprocesos, vea Crear un programa Win32 multiproceso.

Compatibilidad de bibliotecas con el subprocesamiento múltiple

Todas las versiones de CRT ya admiten el subprocesamiento múltiple, a excepción de las versiones que no realizan bloqueo de algunas funciones. Para obtener más información, vea Rendimiento de bibliotecas multiproceso. Para obtener información sobre las versiones de CRT disponibles para vincular con el código, vea Características de la biblioteca de CRT.

Archivos de inclusión para el subprocesamiento múltiple

Los archivos de inclusión de CRT estándar declaran las funciones de la biblioteca en tiempo de ejecución de C cuando se implementan en las bibliotecas. Si las opciones del compilador especifican las convenciones de llamada __fastcall o __vectorcall, el compilador da por hecho que se debe llamar a todas las funciones mediante la convención de llamada del registro. Las funciones de la biblioteca en tiempo de ejecución usan la convención de llamada de C, y las declaraciones de los archivos de inclusión estándar indican al compilador que genere referencias externas correctas a estas funciones.

Funciones de CRT para el control de subprocesos

Todos los programas Win32 contienen al menos un subproceso. Cualquier subproceso puede crear subprocesos adicionales. Un subproceso puede completar su trabajo rápidamente y después terminar, o bien puede permanecer activo durante toda la vida del programa.

Las bibliotecas de CRT proporcionan las siguientes funciones para la creación y finalización de subprocesos: _beginthread, _beginthreadex, _endthread y _endthreadex.

Las funciones _beginthread y _beginthreadex crean un nuevo subproceso y devuelven un identificador del subproceso si la operación se ha realizado correctamente. El subproceso finaliza automáticamente si termina su ejecución. También puede finalizarse a sí mismo con una llamada a _endthread o _endthreadex.

Nota:

Si llama a rutinas en tiempo de ejecución de C desde un programa compilado con libcmt.lib, debe iniciar los subprocesos con la función _beginthread o _beginthreadex. No utilice las funciones ExitThread y CreateThread de Win32. La utilización de SuspendThread puede producir un interbloqueo cuando existe más de un subproceso bloqueado en espera de que el subproceso suspendido complete su acceso a una estructura de datos en tiempo de ejecución de C.

Funciones _beginthread y _beginthreadex

Las funciones _beginthread y _beginthreadex crean un nuevo subproceso. Un subproceso comparte los segmentos de código y de datos de un proceso con otros subprocesos del proceso pero dispone de sus propios y únicos valores de registros, espacio de pila y dirección de la instrucción actual. El sistema asigna tiempo de CPU a cada subproceso, de modo que todos los subprocesos de un proceso puedan ejecutarse de forma simultánea.

_beginthread y _beginthreadex son similares a la función CreateThread de la API Win32, aunque con estas diferencias:

  • Inicializan ciertas variables de la biblioteca en tiempo de ejecución de C. Esto solo importa si se usa la biblioteca en tiempo de ejecución de C en los subprocesos.

  • CreateThread ayuda a proporcionar control sobre los atributos de seguridad. Esta función se puede utilizar para iniciar un subproceso que se encontraba en un estado suspendido.

_beginthread y _beginthreadex devuelven un identificador al nuevo subproceso si se han realizado correctamente o un código de error si se produjo un error.

Funciones _endthread y _endthreadex

La función _endthread termina un subproceso creado por _beginthread (y, de igual forma, _endthreadex termina un subproceso creado por _beginthreadex). Los subprocesos terminan automáticamente cuando finalizan. _endthread y _endthreadex son útiles para la terminación condicional desde un subproceso. Por ejemplo, un subproceso dedicado a procesar las comunicaciones puede terminar si es incapaz de obtener el control del puerto de comunicaciones.

Crear un programa Win32 multiproceso

Cuando se escribe un programa con varios subprocesos, es preciso coordinar su comportamiento y el uso de los recursos del programa. También hay que asegurarse de que cada subproceso disponga de su propia pila.

Uso compartido de recursos comunes entre subprocesos

Nota:

Para acceder a un tema similar desde el punto de vista de MFC, vea Multithreading: sugerencias de programación y Multithreading: cuándo usar las clases de sincronización.

Cada subproceso dispone de su propia pila y su propia copia de los registros de la CPU. Otros recursos, tales como archivos, datos estáticos y memoria dinámica (montón) se comparten entre todos los subprocesos del proceso. Los subprocesos que utilizan estos recursos comunes deben estar sincronizados. Win32 proporciona varios métodos para sincronizar recursos, entre los que se incluyen semáforos, secciones críticas, eventos y exclusiones mutuas (mutex).

Cuando varios subprocesos obtienen acceso a datos estáticos, el programa debe contemplar y solucionar los posibles conflictos que puedan resultar del acceso a los recursos. Imagine un programa en el que un subproceso actualiza una estructura de datos estática que contiene coordenadas x,y para elementos que se van a mostrar mediante otro subproceso. Si el subproceso de actualización modifica la coordenada x y es reemplazado antes de poder cambiar la coordenada y, el subproceso de visualización podría programarse antes de la actualización de la coordenada y. El elemento se mostraría entonces en una posición incorrecta. Este problema se puede evitar mediante la utilización de semáforos que controlen el acceso a la estructura.

Una exclusión mutua (mutex) es un modo de comunicación entre subprocesos o procesos que se ejecutan de forma asincrónica entre sí. Esta comunicación se puede usar para coordinar las actividades de varios procesos o subprocesos, normalmente mediante el control del acceso a un recurso compartido por medio del bloqueo y el desbloqueo del recurso. Para resolver este problema de actualización de las coordenadas x,y, el subproceso de actualización establece una exclusión mutua que indica que la estructura de datos se encuentra en uso antes de realizar la actualización. Posteriormente, después de haber procesado ambas coordenadas, eliminaría la exclusión mutua. El subproceso de presentación en pantalla debe esperar a que se elimine la exclusión mutua para poder actualizar la presentación. Este proceso de espera de una exclusión mutua se suele conocer como bloqueo, ya que el proceso se bloquea y no puede continuar hasta que la exclusión mutua se elimina.

El programa Bounce.c mostrado en Ejemplo de programa multiproceso en C usa una exclusión mutua de nombre ScreenMutex para coordinar las actualizaciones en pantalla. Cada vez que uno de los subprocesos de visualización está listo para escribir en la pantalla, llama a WaitForSingleObject con el identificador de ScreenMutex y la constante INFINITE para indicar que la llamada WaitForSingleObject debe bloquearse en la exclusión mutua y no agotar el tiempo de espera. Si ScreenMutex se ha eliminado, la función wait establece la exclusión mutua para que otros subprocesos no puedan interferir con la visualización y continúa ejecutando el subproceso. De lo contrario, el subproceso se bloquea hasta que la exclusión mutua se elimina. Cuando el subproceso completa la actualización en pantalla, libera la exclusión mutua mediante una llamada a ReleaseMutex.

Las escrituras en pantalla y los datos estáticos son sólo dos de los recursos que requieren una cuidadosa administración. Por ejemplo, el programa puede contener varios subprocesos que tengan acceso al mismo archivo. Puesto que otro subproceso puede haber desplazado el puntero del archivo, cada subproceso debe restablecer ese puntero antes de leer o escribir. Además, cada subproceso debe asegurarse de no ser reemplazado entre el momento en que coloca el puntero hasta que accede al archivo. Estos subprocesos deben usar un semáforo para coordinar el acceso al archivo al poner entre corchetes cada acceso de archivo con llamadas WaitForSingleObject y ReleaseMutex. El siguiente ejemplo de código muestra esta técnica:

HANDLE    hIOMutex = CreateMutex (NULL, FALSE, NULL);

WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

Pilas de subprocesos

Todo el espacio de pila predeterminado de una aplicación se asigna al primer subproceso de ejecución, conocido como subproceso 1. En consecuencia, deberá especificar qué cantidad de memoria desea asignar para una pila independiente para cada subproceso adicional que necesite el programa. El sistema operativo asigna espacio de pila adicional para el subproceso (si es necesario), pero debe especificar un valor predeterminado.

El primer argumento de la llamada _beginthread es un puntero a la función BounceProc, que ejecuta los subprocesos. El segundo argumento especifica el tamaño de pila predeterminado para el subproceso. El último argumento es un número de id. que se pasa a BounceProc. BounceProc usa el número de id. para inicializar el generador de números aleatorios y para seleccionar el atributo de color y el carácter de visualización del subproceso.

Los subprocesos que realizan llamadas a la biblioteca en tiempo de ejecución de C o a la API Win32 deben proporcionar suficiente espacio de pila para la biblioteca y las funciones de la API a las que llaman. La función printf de C requiere más de 500 bytes de espacio de pila, y debería haber 2000 bytes de espacio de pila disponible al llamar a rutinas de la API Win32.

Como cada subproceso dispone de su propia pila, podrá evitar posibles colisiones entre los datos si utiliza la menor cantidad posible de datos estáticos. Diseñe el programa de modo que utilice variables automáticas de pila para todos los datos privados de un subproceso. Las únicas variables globales del programa Bounce.c son exclusiones mutuas o variables que nunca cambian después de su inicialización.

Win32 también proporciona almacenamiento local para el subproceso (TLS) para almacenar datos por subproceso. Para obtener más información, vea Almacenamiento local para el subproceso (TLS).

Evitar áreas de riesgo en programas multiproceso

Pueden surgir varios problemas al crear, vincular o ejecutar un programa de C multiproceso. Algunos de los más comunes se describen en la tabla siguiente. (Para acceder a un tema similar desde el punto de vista de MFC, vea Multithreading: sugerencias de programación).

Problema Causa probable
Aparece un cuadro de mensaje que muestra que el programa ha ocasionado una infracción de protección. Muchos errores de programación de Win32 causan infracciones de protección. Una causa común de las infracciones de protección es la asignación indirecta de datos a punteros nulos. Puesto que da lugar a que el programa intente acceder a una memoria que no le pertenece, se emite una infracción de protección.

Una manera fácil de detectar la causa de una infracción de protección es compilar el programa con información de depuración y luego ejecutarlo por medio del depurador en el entorno de Visual Studio. Cuando se produce el error de protección, Windows transfiere el control al depurador y el cursor se coloca en la línea que ha provocado el problema.
El programa genera numerosos errores de compilación y vinculación. Puede eliminar muchos posibles problemas si establece el nivel de advertencia del compilador en uno de sus valores más altos y hace caso a los mensajes de advertencia. Con las opciones de nivel 3 o 4 de advertencia, puede detectar conversiones de datos involuntarias, prototipos de función que faltan y el uso de características que no son ANSI.

Consulte también

Compatibilidad del código antiguo con multithreading (Visual C++)
Ejemplo de programa multiproceso en C
Almacenamiento local de subprocesos (TLS)
Operaciones simultáneas y asincrónicas con C++/WinRT
Multithreading con C++ y MFC